/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014-2015, Open Source Geospatial Foundation (OSGeo) * (C) 2001-2014 TOPP - www.openplans.org. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.process.raster; import java.awt.Rectangle; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.IOException; import java.util.NoSuchElementException; import javax.media.jai.Interpolation; import javax.media.jai.InterpolationNearest; import javax.media.jai.PlanarImage; import javax.media.jai.iterator.RectIter; import javax.media.jai.iterator.RectIterFactory; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.factory.GeoTools; import org.geotools.feature.DefaultFeatureCollection; import org.geotools.feature.collection.AdaptorFeatureCollection; import org.geotools.feature.collection.BaseSimpleFeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.DirectPosition2D; import org.geotools.geometry.jts.JTS; import org.geotools.geometry.jts.JTSFactoryFinder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.process.ProcessException; import org.geotools.process.factory.DescribeParameter; import org.geotools.process.factory.DescribeProcess; import org.geotools.process.factory.DescribeResult; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.resources.geometry.XRectangle2D; import org.geotools.util.Utilities; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.FeatureType; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Point; /** * A process that wraps a {@link GridCoverage2D} as a collection of point feature. Optional parameters can be set: * <ul> * <li>targetCRS : can be used for calculating the GridConvergence Angle of each point</li> * <li>scale : can be used for scaling the input coverage</li> * <li>interpolation : can be used for setting the interpolation method when Scaling is applied</li> * <li>emisphere : forces to indicate the hemisphere for each point</li> * </ul> * * @author Simone Giannecchini, GeoSolutions * * * @source $URL$ */ @DescribeProcess(title = "Raster As Point Collection", description = "Returns a collection of point features for the pixels of a raster. The band values are provided as attributes.") public class RasterAsPointCollectionProcess implements RasterProcess { @DescribeResult(name = "result", description = "Point features") public SimpleFeatureCollection execute( @DescribeParameter(name = "data", description = "Input raster") GridCoverage2D gc2d, @DescribeParameter(name = "targetCRS", description = "CRS in which the points will be displayed", min=0) CoordinateReferenceSystem targetCRS, @DescribeParameter(name = "scale", description = "scale",min=0, defaultValue="1.0f") Float scaleFactor, @DescribeParameter(name = "interpolation", description = "interpolation",min=0, defaultValue="InterpolationNearest") Interpolation interpolation, @DescribeParameter(name = "emisphere", description = "Add Emishpere",min=0, defaultValue="False" ) Boolean emisphere) throws ProcessException { if (gc2d ==null) { throw new ProcessException("Invalid input, source grid coverage should be not null"); } // Get the GridEnvelope associated to the input Raster for selecting its width and height. // These two values are used for check if the scale is needed GridEnvelope2D gridEnv = gc2d.getGridGeometry().getGridRange2D(); double coverageWidth = gridEnv.getWidth(); double coverageHeight = gridEnv.getHeight(); //// // // scale if/as needed. Also check if the scale expands the coverage dimension for at least 1 pixel. // //// if (scaleFactor != null && (Math.abs(coverageWidth*(scaleFactor - 1f)) >=1 || Math.abs(coverageHeight*(scaleFactor - 1f)) >= 1) ) { // Selection of the interpolation parameter Interpolation interp = interpolation != null ? interpolation : new InterpolationNearest(); // Selection of the ScaleFactors in order to check if the final Raster has almost 1 pixel for Height and Width double scaleX = scaleFactor; double scaleY = scaleFactor; // RenderedImage associated to the coverage final RenderedImage imageToBescaled = gc2d.getRenderedImage(); if (imageToBescaled != null) { final SampleModel sampleModel = imageToBescaled.getSampleModel(); final int height = sampleModel.getHeight(); final int width = sampleModel.getWidth(); if (height * scaleFactor < 1) { scaleY = 1d/height; } if (width * scaleFactor < 1) { scaleX = 1d/width; } } // Execution of the Affine process gc2d = new AffineProcess().execute(gc2d, scaleX, scaleY, null, null, null, null, null, interp); } // return value try { return new RasterAsPointFeatureCollection(gc2d, emisphere, targetCRS); } catch (IOException e) { throw new ProcessException("Unable to wrap provided grid coverage", e); } } /** * TODO @see {@link AdaptorFeatureCollection} * TODO @see {@link DefaultFeatureCollection} * @author Simone Giannecchini, GeoSolutions * */ private final static class RasterAsPointFeatureCollection extends BaseSimpleFeatureCollection { /** * The {@link GeometryFactory} cached here for building points inside iterators */ static final GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory( GeoTools.getDefaultHints() ); /** * The {@link GridCoverage2D} that we want to expose as a point feature collection. */ final GridCoverage2D gc2d; /** * Number of points in this collections is as many as width*height. */ final int size; /** * Grid to world transformation at the upper left corner of the raster space. */ final MathTransform2D mt2D; /** * The bounding box for this feature collection */ private ReferencedEnvelope bounds; /** * Raster bounds for this coverage */ final Rectangle rasterBounds; /** * Number of bands */ final int numBands; /** * Boolean indicating if the Hemisphere in which the point lies must be indicated */ private boolean emisphere; /** * Optional transformation from the coverage CRS to WGS84 */ private MathTransform transformToWGS84; /** * Index for the North Dimension of the coverage CRS */ private int northDimension=-1; /** * Target CRS indicating that the input Coverage has been reprojected */ private CoordinateReferenceSystem targetCRS; /** * Transformation used for reprojecting each point from the coverage CRS to the target CRS */ private MathTransform reprojectionTransformation; /** * Boolean indicating if the calculation of the GridConvergence Angle is requested */ private boolean gridConvergenceAngleCorrectionNeeded; /** * Class used for calculating the GridConvergence Angle */ private GridConvergenceAngleCalc gridConvergenceAngleManager; public RasterAsPointFeatureCollection(final GridCoverage2D gc2d) throws IOException { this(gc2d, false, gc2d.getCoordinateReferenceSystem2D()); } /** * @param gc2d2 * @param emisphere * @param targetCRS */ public RasterAsPointFeatureCollection(GridCoverage2D gc2d, boolean emisphere, CoordinateReferenceSystem targetCRS)throws IOException { super(modify(CoverageUtilities.createFeatureType(gc2d, Point.class),emisphere,targetCRS)); this.gc2d=gc2d; this.emisphere=emisphere; this.targetCRS=targetCRS; // // get various elements from this coverage // // SIZE final RenderedImage raster=gc2d.getRenderedImage(); size=raster.getWidth()*raster.getHeight(); // GRID TO WORLD for transforming raster points into model points mt2D= gc2d.getGridGeometry().getGridToCRS2D(PixelOrientation.UPPER_LEFT); // BOUNDS take into account that we want to map center coordinates rasterBounds = PlanarImage.wrapRenderedImage(raster).getBounds(); final XRectangle2D rasterBounds_=new XRectangle2D(raster.getMinX()+0.5, raster.getMinY()+0.5, raster.getWidth()-1, raster.getHeight()-1); try { bounds = new ReferencedEnvelope(CRS.transform(mt2D, rasterBounds_, null), gc2d.getCoordinateReferenceSystem2D()); } catch (Exception e) { final IOException ioe = new IOException(); ioe.initCause(e); throw ioe; } // BANDS numBands = gc2d.getNumSampleDimensions(); final CoordinateReferenceSystem coverageCRS=gc2d.getCoordinateReferenceSystem2D(); // // Emisphere management // emisphereManagement(coverageCRS); // // Grid Convergence Angle correction // gridConvergenceAngle(coverageCRS); } /** * Prepare for Managing the GridConvergenceAngle * * @param coverageCRS the {@link GridCoverage2D} {@link CoordinateReferenceSystem} */ private void gridConvergenceAngle(final CoordinateReferenceSystem coverageCRS) { // GridCoverage Angle management is required only if the input Coverage has been reprojected if (targetCRS != null) { // there is a real reprojection? if (!CRS.equalsIgnoreMetadata(coverageCRS, targetCRS)) { // get the transformation and check if that is not the identity try { reprojectionTransformation = CRS.findMathTransform(coverageCRS, targetCRS, true); gridConvergenceAngleCorrectionNeeded = !reprojectionTransformation .isIdentity(); if (gridConvergenceAngleCorrectionNeeded) { // // Instantiate calculator to use to determine convergence angle at // each point for the request CRS. // gridConvergenceAngleManager = new GridConvergenceAngleCalc(targetCRS); } } catch (Exception e) { new IOException(e); } } } } /** * Prepare the variables used by the iterator for checking the North Hemisphere. * * @param coverageCRS CRS of the input coverage * @throws IOException */ private void emisphereManagement(CoordinateReferenceSystem coverageCRS) throws IOException { // The Hemisphere is evaluated only if the associated flag is set to true if (emisphere) { // If the Coverage CRS is already in WGS84 then is is easy to get the Hemisphere from the Y direction value, // else a reprojection is needed. if (!CRS.equalsIgnoreMetadata(DefaultGeographicCRS.WGS84, coverageCRS)) { try { // save transform final MathTransform transform = CRS.findMathTransform(coverageCRS, DefaultGeographicCRS.WGS84, true); if (!transform.isIdentity()) { this.transformToWGS84 = transform; } final CoordinateSystem coordinateSystem = coverageCRS.getCoordinateSystem(); // save also latitude axis final int dimension = coordinateSystem.getDimension(); for (int i = 0; i < dimension; i++) { CoordinateSystemAxis axis = coordinateSystem.getAxis(i); if (axis.getDirection().absolute().compareTo(AxisDirection.NORTH) == 0) { this.northDimension = i; break; } } // If the northDimension has not been found then an exception is thrown if (northDimension < 0) { final IOException ioe = new IOException( "Unable to find nort dimension in the coverage CRS+ " + coverageCRS.toWKT()); throw ioe; } } catch (FactoryException e) { throw new IOException(e); } } } } /** * Static method which prepares the feature type associated to each Point. * * @param featureType Input {@link FeatureType} associated to the Coverage * @param emisphere Boolean indicating if the emisphere must be set * @param targetCRS CRS used if the gridConvergence Angle must be calculated * @return */ private static SimpleFeatureType modify(SimpleFeatureType featureType, boolean emisphere, CoordinateReferenceSystem targetCRS) { if (!emisphere && targetCRS == null) { return featureType; } // add emishpere attribute final SimpleFeatureTypeBuilder ftBuilder = new SimpleFeatureTypeBuilder(); ftBuilder.setName(featureType.getName()); // CRS ftBuilder.setCRS(featureType.getCoordinateReferenceSystem()); // TYPE is as follows the_geom | band ftBuilder.setDefaultGeometry(featureType.getGeometryDescriptor().getLocalName()); // copy attributes for (AttributeDescriptor atd : featureType.getAttributeDescriptors()) { ftBuilder.add(atd); } if (emisphere) { // add emisphere ftBuilder.add("emisphere", String.class); // valid values N/S } if (targetCRS != null) { // add gridConvergenceAngle correction ftBuilder.add("gridConvergenceAngleCorrection", Double.class); // degrees } return ftBuilder.buildFeatureType(); } @Override public SimpleFeatureIterator features() { return new RasterAsPointFeatureIterator(this); } @Override public int size() { return size; } @Override public ReferencedEnvelope getBounds() { return new ReferencedEnvelope(bounds); } } private final static class RasterAsPointFeatureIterator implements SimpleFeatureIterator { private final double[] temp; private final SimpleFeatureBuilder fb; private final RasterAsPointFeatureCollection fc; private int index=0; private final int size; private final RectIter iterator; private final Coordinate sourceCoordinate = new Coordinate(); /** Position of the Point in the source CRS*/ private DirectPosition2D sourceCRSPosition; /** Position of the Point in the target CRS*/ private DirectPosition2D targetCRSPosition; public RasterAsPointFeatureIterator(final RasterAsPointFeatureCollection fc) { // checks Utilities.ensureNonNull("fc", fc); // get elements this.fc = fc; this.fb = new SimpleFeatureBuilder(fc.getSchema()); this.size=fc.size; // create an iterator that only goes forward, it is the fastest one iterator= RectIterFactory.create(fc.gc2d.getRenderedImage(), null); // //start the iterator // iterator.startLines(); if(iterator.finishedLines()){ throw new NoSuchElementException("Index beyond size:"+index+">"+size); } iterator.startPixels(); if(iterator.finishedPixels()){ throw new NoSuchElementException("Index beyond size:"+index+">"+size); } // appo temp= new double[fc.numBands]; // grid convergence angle manager if(fc.gridConvergenceAngleCorrectionNeeded){ sourceCRSPosition = new DirectPosition2D(); targetCRSPosition = new DirectPosition2D(fc.targetCRS); } } /** * Closes this iterator */ public void close() { // NO OP } /** * Tells us whether or not we have more elements to iterate on. */ public boolean hasNext() { return index<size; } public SimpleFeature next() throws NoSuchElementException { if(!hasNext()){ throw new NoSuchElementException("Index beyond size:"+index+">"+size); } // iterate if(iterator.finishedPixels()){ throw new NoSuchElementException("Index beyond size:"+index+">"+size); } if(iterator.finishedLines()){ throw new NoSuchElementException("Index beyond size:"+index+">"+size); } // ID final int id=index; // POINT // can we reuse the coord? sourceCoordinate.x= 0.5+ fc.rasterBounds.x+index%fc.rasterBounds.width; sourceCoordinate.y= 0.5+ fc.rasterBounds.y+index/fc.rasterBounds.width ; Point point = RasterAsPointFeatureCollection.geometryFactory.createPoint( sourceCoordinate ); try { point=(Point) JTS.transform(point, fc.mt2D); fb.add(point); // VALUES // loop on bands iterator.getPixel(temp); for (double d : temp) { // I exploit the internal converters to go from double to whatever the type is // TODO is this correct or we can do more. fb.add(d); } // Hemisphere management emisphereAttributeManagement(point); // grid convergence angle correction gridConvergenceAngleManagement(point); } catch (Exception e) { final NoSuchElementException nse = new NoSuchElementException(); nse.initCause(e); throw nse; } // do we need to wrap the line?? if(iterator.nextPixelDone()){ if(!iterator.nextLineDone()) iterator.startPixels(); } // return final SimpleFeature returnValue= fb.buildFeature(String.valueOf(id)); // increase index and iterator index++; return returnValue; } /** * This method adds the GridConvergence Angle attribute to the Point feature if it is needed. * * @param point Input Point to handle. */ private void gridConvergenceAngleManagement(Point point) throws Exception { // If the GridConvergence Angle correction is not requested, then nothing is done if (fc.gridConvergenceAngleCorrectionNeeded) { // // Calculate convergence angle // sourceCRSPosition.setLocation(point.getX(), point.getY()); fc.reprojectionTransformation.transform(sourceCRSPosition, targetCRSPosition); // Calculation of the Angle double convAngle = fc.gridConvergenceAngleManager .getConvergenceAngle(targetCRSPosition); fb.add(convAngle); } } /** * This method adds the Hemisphere attribute to the Point feature if requested. * * @param point Input Point to handle. * @throws NoSuchElementException */ private void emisphereAttributeManagement(final Point point) throws IOException { // If the Hemisphere flag is set to false no calculation is performed if (fc.emisphere) { // If the Coverage CRS is WGS84 then the Y coordinate value indicates the associated Hemisphere if (fc.transformToWGS84 == null) { if (point.getY() >= 0) { fb.add("N"); } else { fb.add("S"); } } else { try { // Else the point must be reprojected previously in order to define the Hemisphere Point wgs84Point = (Point) JTS.transform(point, fc.transformToWGS84); if (wgs84Point.getCoordinate().getOrdinate(fc.northDimension) >= 0) { fb.add("N"); } else { fb.add("S"); } } catch (Exception e) { throw new IOException(e); } } } } } }