/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2011, Open Source Geospatial Foundation (OSGeo) * (C) 2001-2007 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.gs; import java.awt.geom.AffineTransform; import java.awt.image.RenderedImage; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.media.jai.JAI; import javax.media.jai.ParameterBlockJAI; import javax.media.jai.RenderedOp; import org.jaitools.media.jai.vectorize.VectorizeDescriptor; import org.jaitools.media.jai.vectorize.VectorizeRIF; import org.jaitools.numeric.Range; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.util.AffineTransformation; import org.geotools.process.factory.DescribeParameter; import org.geotools.process.factory.DescribeProcess; import org.geotools.process.factory.DescribeResult; import org.geotools.process.gs.GSProcess; import org.geotools.process.raster.CoverageUtilities; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.data.collection.ListFeatureCollection; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.image.jai.Registry; import org.geotools.process.ProcessException; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.util.ProgressListener; /** * A process for raster to vector conversion. Regions of uniform value in an * input {@linkplain GridCoverage2D} are converted into {@linkplain Polygon}s * by tracing the cell boundaries. Results are returned as a {@linkplain * SimpleFeatureCollection} in which each feature corresponds to a raster * region with the boundary {@code Polygon} as its default geometry ("the_geom") * and the value of the raster region cells as an attribute ("value"). * <p> * Optionally, a list of classification ranges ({@linkplain org.jaitools.numeric.Range} * objects) can be provided to pre-classify the input coverage values into intervals. * Vectorizing can also be restricted to a sub-area of the coverage and/or a subset * of raster values (by defining values to treat as no-data). * * @author Simone Giannecchini, GeoSolutions * @since 8.0 * * @source $URL$ * @version $Id$ */ @DescribeProcess(title = "PolygonExtraction", description = "Perform the polygon extraction on a provided raster") public class PolygonExtractionProcess implements GSProcess { static { Registry.registerRIF(JAI.getDefaultInstance(), new VectorizeDescriptor(), new VectorizeRIF(), Registry.JAI_TOOLS_PRODUCT); } /** * Executes the raster to vector process. * * @param coverage the input grid coverage * * @param band the coverage band to process; defaults to 0 if {@code null} * * @param insideEdges whether boundaries between raster regions with data values * (ie. not NODATA) should be returned; defaults to {@code true} if {@code null} * * @param roi optional polygonal {@code Geometry} to define a sub-area within which * vectorizing will be done * * @param noDataValues optional list of values to treat as NODATA; regions with these * values will not be represented in the returned features; * if {@code null}, 0 is used as the single NODATA value; ignored if {@code * classificationRanges} is provided * * @param classificationRanges optional list of {@code Range} objects to pre-classify * the input coverage prior to vectorizing; values not included in the list will be * treated as NODATA; values in the first {@code Range} are classified to 1, those * in the second {@code Range} to 2 etc. * * @param progressListener an optional listener * * @return a feature collection where each feature has a {@code Polygon} ("the_geom") * and an attribute "value" with value of the corresponding region in either * {@code coverage} or the classified coverage (when {@code classificationRanges} * is used) * * @throws ProcessException */ @DescribeResult(name = "result", description = "The polygon feature collection") public SimpleFeatureCollection execute( @DescribeParameter(name = "data", description = "The raster to be used as the source") GridCoverage2D coverage, @DescribeParameter(name = "band", description = "(Integer, default=0) the source image band to process", min = 0) Integer band, @DescribeParameter(name = "insideEdges", description = "(Boolean, default=true) whether to vectorize boundaries between adjacent regions with non-outside values", min = 0) Boolean insideEdges, @DescribeParameter(name = "roi", description = "The geometry used to delineate the area of interest in model space", min = 0) Geometry roi, @DescribeParameter(name = "nodata", description = "Collection<Number>, default={0}) values to treat as NODATA", collectionType = Number.class, min = 0) Collection<Number> noDataValues, @DescribeParameter(name = "ranges", description = "The list of ranges to be applied. \n" + "Each range is expressed as 'OPEN START ; END CLOSE'\n" + "where 'OPEN:=(|[, CLOSE=)|]',\n " + "START is the low value, or nothing to imply -INF,\n" + "CLOSE is the biggest value, or nothing to imply +INF", collectionType = Range.class, min = 0) List<Range> classificationRanges, ProgressListener progressListener) throws ProcessException { // // initial checks // if (coverage == null) { throw new ProcessException("Invalid input, source grid coverage should be not null"); } if (band == null) { band = 0; } else if (band < 0 || band >= coverage.getNumSampleDimensions()) { throw new ProcessException("Invalid input, invalid band number:" + band); } // do we have classification ranges? boolean hasClassificationRanges = classificationRanges != null && classificationRanges.size() > 0; // apply the classification by setting 0 as the default value and using 1, ..., numClasses for the other classes. // we use 0 also as the noData for the resulting coverage. if (hasClassificationRanges) { final RangeLookupProcess lookup = new RangeLookupProcess(); coverage = lookup.execute( coverage, band, classificationRanges, progressListener); } // Use noDataValues to set the "outsideValues" parameter of the Vectorize // operation unless classificationRanges are in use, in which case the // noDataValues arg is ignored. List<Number> outsideValues = new ArrayList<Number>(); if (noDataValues != null && !hasClassificationRanges) { outsideValues.addAll(noDataValues); } else { outsideValues.add(0); } // // GRID TO WORLD preparation // final AffineTransform mt2D = (AffineTransform) coverage.getGridGeometry().getGridToCRS2D(PixelOrientation.UPPER_LEFT); // get the rendered image final RenderedImage raster = coverage.getRenderedImage(); // perform jai operation ParameterBlockJAI pb = new ParameterBlockJAI("Vectorize"); pb.setSource("source0", raster); if (roi != null) { pb.setParameter("roi", CoverageUtilities.prepareROI(roi, mt2D)); } pb.setParameter("band", band); pb.setParameter("outsideValues", outsideValues); if (insideEdges != null) { pb.setParameter("insideEdges", insideEdges); } // pb.setParameter("removeCollinear", false); final RenderedOp dest = JAI.create("Vectorize", pb); @SuppressWarnings("unchecked") final Collection<Polygon> prop = (Collection<Polygon>) dest.getProperty(VectorizeDescriptor.VECTOR_PROPERTY_NAME); // wrap as a feature collection and return final SimpleFeatureType featureType = CoverageUtilities.createFeatureType(coverage, Polygon.class); final SimpleFeatureBuilder builder = new SimpleFeatureBuilder(featureType); int i = 0; final ListFeatureCollection featureCollection = new ListFeatureCollection(featureType); final AffineTransformation jtsTransformation = new AffineTransformation( mt2D.getScaleX(), mt2D.getShearX(), mt2D.getTranslateX(), mt2D.getShearY(), mt2D.getScaleY(), mt2D.getTranslateY()); for (Polygon polygon : prop) { // get value Double value = (Double) polygon.getUserData(); polygon.setUserData(null); // filter coordinates in place polygon.apply(jtsTransformation); // create feature and add to list builder.set("the_geom", polygon); builder.set("value", value); featureCollection.add(builder.buildFeature(String.valueOf(i++))); } //return value return featureCollection; } }