/** * Creates a {@link SimpleFeatureType} that exposes a coverage as a collections of feature points, * mapping the centre of each pixel as a point plus all the bands as attributes. * * <p>The FID is the long that combines x+y*width. * * @param gc2d the {@link GridCoverage2D} to wrap. * @param geometryClass the class for the geometry. * @return a {@link SimpleFeatureType} or <code>null</code> in case we are unable to wrap the * coverage */ public static SimpleFeatureType createFeatureType( final GridCoverage2D gc2d, final Class<? extends Geometry> geometryClass) { // checks Utilities.ensureNonNull("gc2d", gc2d); // building a feature type for this coverage final SimpleFeatureTypeBuilder ftBuilder = new SimpleFeatureTypeBuilder(); ftBuilder.setName(gc2d.getName().toString()); ftBuilder.setNamespaceURI("http://www.geotools.org/"); // CRS ftBuilder.setCRS(gc2d.getCoordinateReferenceSystem2D()); // ftBuilder.setCRS(DefaultEngineeringCRS.GENERIC_2D); // TYPE is as follows the_geom | band ftBuilder.setDefaultGeometry("the_geom"); ftBuilder.add("the_geom", geometryClass); if (!geometryClass.equals(Point.class)) { ftBuilder.add("value", Double.class); } else { // get sample type on bands final GridSampleDimension[] sampleDimensions = gc2d.getSampleDimensions(); for (GridSampleDimension sd : sampleDimensions) { final SampleDimensionType sdType = sd.getSampleDimensionType(); final int dataBuffType = TypeMap.getDataBufferType(sdType); // TODO I think this should be a public utility inside the FeatureUtilities class @SuppressWarnings("rawtypes") final Class bandClass; switch (dataBuffType) { case DataBuffer.TYPE_BYTE: bandClass = Byte.class; break; case DataBuffer.TYPE_DOUBLE: bandClass = Double.class; break; case DataBuffer.TYPE_FLOAT: bandClass = Float.class; break; case DataBuffer.TYPE_INT: bandClass = Integer.class; break; case DataBuffer.TYPE_SHORT: case DataBuffer.TYPE_USHORT: bandClass = Short.class; break; case DataBuffer.TYPE_UNDEFINED: default: return null; } ftBuilder.add(sd.getDescription().toString(), bandClass); } } return ftBuilder.buildFeatureType(); }
/** * Performs a translation using the "Resample" operation. * * @param grid the {@link GridCoverage2D} to apply the translation on. * @throws NoninvertibleTransformException If a "grid to CRS" transform is not invertible. */ private void doTranslation(GridCoverage2D grid) throws NoninvertibleTransformException { final int transX = -253; final int transY = -456; final double scaleX = 0.04; final double scaleY = -0.04; final ParameterBlock block = new ParameterBlock() .addSource(grid.getRenderedImage()) .add((float) transX) .add((float) transY); RenderedImage image = JAI.create("Translate", block); assertEquals("Incorrect X translation", transX, image.getMinX()); assertEquals("Incorrect Y translation", transY, image.getMinY()); /* * Create a grid coverage from the translated image but with the same envelope. * Consequently, the 'gridToCoordinateSystem' should be translated by the same * amount, with the opposite sign. */ AffineTransform expected = getAffineTransform(grid); assertNotNull(expected); expected = new AffineTransform(expected); // Get a mutable instance. final GridCoverageFactory factory = CoverageFactoryFinder.getGridCoverageFactory(null); grid = factory.create( "Translated", image, grid.getEnvelope(), grid.getSampleDimensions(), new GridCoverage2D[] {grid}, grid.getProperties()); expected.translate(-transX, -transY); assertTransformEquals(expected, getAffineTransform(grid)); /* * Apply the "Resample" operation with a specific 'gridToCoordinateSystem' transform. * The envelope is left unchanged. The "Resample" operation should compute automatically * new image bounds. */ final AffineTransform at = AffineTransform.getScaleInstance(scaleX, scaleY); final MathTransform tr = ProjectiveTransform.create(at); // account for the half pixel correction between the two spaces since we are talking raster here // but the resample will talk model! final MathTransform correctedTransform = PixelTranslation.translate(tr, PixelInCell.CELL_CORNER, PixelInCell.CELL_CENTER); final GridGeometry2D geometry = new GridGeometry2D(null, correctedTransform, null); final GridCoverage2D newGrid = (GridCoverage2D) Operations.DEFAULT.resample(grid, grid.getCoordinateReferenceSystem(), geometry, null); assertEquals(correctedTransform, getAffineTransform(newGrid)); image = newGrid.getRenderedImage(); expected.preConcatenate(at.createInverse()); final Point point = new Point(transX, transY); assertSame(point, expected.transform(point, point)); // Round toward neareast integer }
/** * Encodes the RangeType as per the GML spec of the provided {@link GridCoverage2D} * * <p>e.g.: * * <pre>{@code * <gmlcov:rangeType> * <swe:DataRecord> * <swe:field name="singleBand"> * <swe:Quantity definition="http://www.opengis.net/def/property/OGC/0/Radiance"> * <gml:description>Panchromatic Channel</gml:description> * <gml:name>single band</gml:name> * <swe:uom code="W/cm2"/> * <swe:constraint> * <swe:AllowedValues> * <swe:interval>0 255</swe:interval> * <swe:significantFigures>3</swe:significantFigures> * </swe:AllowedValues> * </swe:constraint> * </swe:Quantity> * </swe:field> * </swe:DataRecord> * </gmlcov:rangeType> * }</pre> * * @param gc2d the {@link GridCoverage2D} for which to encode the RangeType. */ public void handleRangeType(GridCoverage2D gc2d) { start("gml:rangeType"); start("swe:DataRecord"); // get bands final SampleDimension[] bands = gc2d.getSampleDimensions(); // handle bands for (SampleDimension sd : bands) { final AttributesImpl fieldAttr = new AttributesImpl(); fieldAttr.addAttribute( "", "name", "name", "", sd.getDescription().toString()); // TODO NCNAME? TODO Use Band[i] convention? start("swe:field", fieldAttr); start("swe:Quantity"); // Description start("swe:description"); chars(sd.getDescription().toString()); // TODO can we make up something better?? end("swe:description"); // UoM final AttributesImpl uomAttr = new AttributesImpl(); final Unit<?> uom = sd.getUnits(); uomAttr.addAttribute( "", "code", "code", "", uom == null ? "W.m-2.Sr-1" : UnitFormat.getInstance().format(uom)); start("swe:uom", uomAttr); end("swe:uom"); // constraint on values start("swe:constraint"); start("swe:AllowedValues"); handleSampleDimensionRange(sd); // TODO make this generic end("swe:AllowedValues"); end("swe:constraint"); // nil values handleSampleDimensionNilValues(gc2d, sd.getNoDataValues()); end("swe:Quantity"); end("swe:field"); } end("swe:DataRecord"); end("gml:rangeType"); }
/** * Applies the band select operation to a grid coverage. * * @param cropEnvelope the target envelope; always not null * @param cropROI the target ROI shape; nullable * @param roiTolerance; as read from op's params * @param sourceCoverage is the source {@link GridCoverage2D} that we want to crop. * @param hints A set of rendering hints, or {@code null} if none. * @param sourceGridToWorldTransform is the 2d grid-to-world transform for the source coverage. * @return The result as a grid coverage. */ private static GridCoverage2D buildResult( final GeneralEnvelope cropEnvelope, final Geometry cropROI, final double roiTolerance, final boolean forceMosaic, final Hints hints, final GridCoverage2D sourceCoverage, final AffineTransform sourceGridToWorldTransform) { // // Getting the source coverage and its child geolocation objects // final RenderedImage sourceImage = sourceCoverage.getRenderedImage(); final GridGeometry2D sourceGridGeometry = ((GridGeometry2D) sourceCoverage.getGridGeometry()); final GridEnvelope2D sourceGridRange = sourceGridGeometry.getGridRange2D(); // // Now we try to understand if we have a simple scale and translate or a // more elaborated grid-to-world transformation n which case a simple // crop could not be enough, but we may need a more elaborated chain of // operation in order to do a good job. As an instance if we // have a rotation which is not multiple of PI/2 we have to use // the mosaic with a ROI // final boolean isSimpleTransform = CoverageUtilities.isSimpleGridToWorldTransform(sourceGridToWorldTransform, EPS); // Do we need to explode the Palette to RGB(A)? // int actionTaken = 0; // // // // Layout // // // final RenderingHints targetHints = new RenderingHints(null); if (hints != null) targetHints.add(hints); final ImageLayout layout = initLayout(sourceImage, targetHints); targetHints.put(JAI.KEY_IMAGE_LAYOUT, layout); // // prepare the processor to use for this operation // final JAI processor = OperationJAI.getJAI(targetHints); final boolean useProvidedProcessor = !processor.equals(JAI.getDefaultInstance()); try { if (cropROI != null) { // replace the cropEnvelope with the envelope of the intersection // of the ROI and the cropEnvelope. // Remember that envelope(intersection(roi,cropEnvelope)) != intersection(cropEnvelope, // envelope(roi)) final Polygon modelSpaceROI = FeatureUtilities.getPolygon(cropEnvelope, GFACTORY); Geometry intersection = IntersectUtils.intersection(cropROI, modelSpaceROI); Envelope2D e2d = JTS.getEnvelope2D( intersection.getEnvelopeInternal(), cropEnvelope.getCoordinateReferenceSystem()); GeneralEnvelope ge = new GeneralEnvelope((org.opengis.geometry.Envelope) e2d); cropEnvelope.setEnvelope(ge); } // // // // Build the new range by keeping into // account translation of grid geometry constructor for respecting // OGC PIXEL-IS-CENTER ImageDatum assumption. // // // final AffineTransform sourceWorldToGridTransform = sourceGridToWorldTransform.createInverse(); // // // // finalRasterArea will hold the smallest rectangular integer raster area that contains the // floating point raster // area which we obtain when applying the world-to-grid transform to the cropEnvelope. Note // that we need to intersect // such an area with the area covered by the source coverage in order to be sure we do not try // to crop outside the // bounds of the source raster. // // // final Rectangle2D finalRasterAreaDouble = XAffineTransform.transform( sourceWorldToGridTransform, cropEnvelope.toRectangle2D(), null); final Rectangle finalRasterArea = finalRasterAreaDouble.getBounds(); // intersection with the original range in order to not try to crop outside the image bounds Rectangle.intersect(finalRasterArea, sourceGridRange, finalRasterArea); if (finalRasterArea.isEmpty()) throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP)); // // // // It is worth to point out that doing a crop the G2W transform // should not change while the envelope might change as // a consequence of the rounding of the underlying image datum // which uses integer factors or in case the G2W is very // complex. Note that we will always strive to // conserve the original grid-to-world transform. // // // // we do not have to crop in this case (should not really happen at // this time) if (finalRasterArea.equals(sourceGridRange) && isSimpleTransform && cropROI == null) return sourceCoverage; // // // // if I get here I have something to crop // using the world-to-grid transform for going from envelope to the // new grid range. // // // final double minX = finalRasterArea.getMinX(); final double minY = finalRasterArea.getMinY(); final double width = finalRasterArea.getWidth(); final double height = finalRasterArea.getHeight(); // // // // Check if we need to use mosaic or crop // // // final PlanarImage croppedImage; final ParameterBlock pbj = new ParameterBlock(); pbj.addSource(sourceImage); java.awt.Polygon rasterSpaceROI = null; String operatioName = null; if (!isSimpleTransform || cropROI != null) { // ///////////////////////////////////////////////////////////////////// // // We don't have a simple scale and translate transform, JAI // crop MAY NOT suffice. Let's decide whether or not we'll use // the Mosaic. // // ///////////////////////////////////////////////////////////////////// Polygon modelSpaceROI = FeatureUtilities.getPolygon(cropEnvelope, GFACTORY); // // // // Now convert this polygon back into a shape for the source // raster space. // // // final List<Point2D> points = new ArrayList<Point2D>(5); rasterSpaceROI = FeatureUtilities.convertPolygonToPointArray( modelSpaceROI, ProjectiveTransform.create(sourceWorldToGridTransform), points); if (rasterSpaceROI == null || rasterSpaceROI.getBounds().isEmpty()) if (finalRasterArea.isEmpty()) throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP)); final boolean doMosaic = forceMosaic ? true : decideJAIOperation(roiTolerance, rasterSpaceROI.getBounds2D(), points); if (doMosaic || cropROI != null) { // prepare the params for the mosaic final ROI[] roiarr; try { if (cropROI != null) { final LiteShape2 cropRoiLS2 = new LiteShape2( cropROI, ProjectiveTransform.create(sourceWorldToGridTransform), null, false); ROI cropRS = new ROIShape(cropRoiLS2); Rectangle2D rt = cropRoiLS2.getBounds2D(); if (!hasIntegerBounds(rt)) { // Approximate Geometry Geometry geo = (Geometry) cropRoiLS2.getGeometry().clone(); transformGeometry(geo); cropRS = new ROIShape(new LiteShape2(geo, null, null, false)); } roiarr = new ROI[] {cropRS}; } else { final ROIShape roi = new ROIShape(rasterSpaceROI); roiarr = new ROI[] {roi}; } } catch (FactoryException ex) { throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), ex); } pbj.add(MosaicDescriptor.MOSAIC_TYPE_OVERLAY); pbj.add(null); pbj.add(roiarr); pbj.add(null); pbj.add(CoverageUtilities.getBackgroundValues(sourceCoverage)); // prepare the final layout final Rectangle bounds = rasterSpaceROI.getBounds2D().getBounds(); Rectangle.intersect(bounds, sourceGridRange, bounds); if (bounds.isEmpty()) throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP)); // we do not have to crop in this case (should not really happen at // this time) if (!doMosaic && bounds.getBounds().equals(sourceGridRange) && isSimpleTransform) return sourceCoverage; // nice trick, we use the layout to do the actual crop final Rectangle boundsInt = bounds.getBounds(); layout.setMinX(boundsInt.x); layout.setWidth(boundsInt.width); layout.setMinY(boundsInt.y); layout.setHeight(boundsInt.height); operatioName = "Mosaic"; } } // do we still have to set the operation name? If so that means we have to go for crop. if (operatioName == null) { // executing the crop pbj.add((float) minX); pbj.add((float) minY); pbj.add((float) width); pbj.add((float) height); operatioName = "GTCrop"; } // // // // Apply operation // // // if (!useProvidedProcessor) { croppedImage = JAI.create(operatioName, pbj, targetHints); } else { croppedImage = processor.createNS(operatioName, pbj, targetHints); } // conserve the input grid to world transformation Map sourceProperties = sourceCoverage.getProperties(); Map properties = null; if (sourceProperties != null && !sourceProperties.isEmpty()) { properties = new HashMap(sourceProperties); } if (rasterSpaceROI != null) { if (properties != null) { properties.put("GC_ROI", rasterSpaceROI); } else { properties = Collections.singletonMap("GC_ROI", rasterSpaceROI); } } return new GridCoverageFactory(hints) .create( sourceCoverage.getName(), croppedImage, new GridGeometry2D( new GridEnvelope2D(croppedImage.getBounds()), sourceGridGeometry.getGridToCRS2D(PixelOrientation.CENTER), sourceCoverage.getCoordinateReferenceSystem()), (GridSampleDimension[]) (actionTaken == 1 ? null : sourceCoverage.getSampleDimensions().clone()), new GridCoverage[] {sourceCoverage}, properties); } catch (TransformException e) { throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), e); } catch (NoninvertibleTransformException e) { throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), e); } }
@DescribeResult(name = "result", description = "The contours feature collection") public SimpleFeatureCollection execute( @DescribeParameter(name = "data", description = "The raster to be used as the source") GridCoverage2D gc2d, @DescribeParameter( name = "band", description = "The source image band to process", min = 0, max = 1) Integer band, @DescribeParameter(name = "levels", description = "Values for which to generate contours") double[] levels, @DescribeParameter( name = "interval", description = "Interval between contour values (ignored if levels arg is supplied)", min = 0) Double interval, @DescribeParameter( name = "simplify", description = "Values for which to generate contours", min = 0) Boolean simplify, @DescribeParameter( name = "smooth", description = "Values for which to generate contours", min = 0) Boolean smooth, @DescribeParameter( name = "roi", description = "The geometry used to delineate the area of interest in model space", min = 0) Geometry roi, ProgressListener progressListener) throws ProcessException { // // initial checks // if (gc2d == null) { throw new ProcessException("Invalid input, source grid coverage should be not null"); } if (band != null && (band < 0 || band >= gc2d.getNumSampleDimensions())) { throw new ProcessException("Invalid input, invalid band number:" + band); } boolean hasValues = !(levels == null || levels.length == 0); if (!hasValues && interval == null) { throw new ProcessException("One between interval and values must be valid"); } // switch to geophisics if necessary gc2d = gc2d.view(ViewType.GEOPHYSICS); // // GRID TO WORLD preparation // final AffineTransform mt2D = (AffineTransform) gc2d.getGridGeometry().getGridToCRS2D(PixelOrientation.CENTER); // get the list of nodata, if any List<Object> noDataList = new ArrayList<Object>(); for (GridSampleDimension sd : gc2d.getSampleDimensions()) { // grab all the explicit nodata final double[] sdNoData = sd.getNoDataValues(); if (sdNoData != null) { for (double nodata : sdNoData) { noDataList.add(nodata); } } // handle also readers setting up nodata in a category with a specific name if (sd.getCategories() != null) { for (Category cat : sd.getCategories()) { if (cat.getName().equals(NO_DATA)) { final NumberRange<? extends Number> catRange = cat.getRange(); if (catRange.getMinimum() == catRange.getMaximum()) { noDataList.add(catRange.getMinimum()); } else { Range<Double> noData = new Range<Double>( catRange.getMinimum(), catRange.isMinIncluded(), catRange.getMaximum(), catRange.isMaxIncluded()); noDataList.add(noData); } } } } } // get the rendered image final RenderedImage raster = gc2d.getRenderedImage(); // perform jai operation ParameterBlockJAI pb = new ParameterBlockJAI("Contour"); pb.setSource("source0", raster); if (roi != null) { pb.setParameter("roi", CoverageUtilities.prepareROI(roi, mt2D)); } if (band != null) { pb.setParameter("band", band); } if (interval != null) { pb.setParameter("interval", interval); } else { final ArrayList<Double> elements = new ArrayList<Double>(levels.length); for (double level : levels) elements.add(level); pb.setParameter("levels", elements); } if (simplify != null) { pb.setParameter("simplify", simplify); } if (smooth != null) { pb.setParameter("smooth", smooth); } if (noDataList != null) { pb.setParameter("nodata", noDataList); } final RenderedOp dest = JAI.create("Contour", pb); @SuppressWarnings("unchecked") final Collection<LineString> prop = (Collection<LineString>) dest.getProperty(ContourDescriptor.CONTOUR_PROPERTY_NAME); // wrap as a feature collection and return final SimpleFeatureType schema = CoverageUtilities.createFeatureType(gc2d, LineString.class); final SimpleFeatureBuilder builder = new SimpleFeatureBuilder(schema); int i = 0; final ListFeatureCollection featureCollection = new ListFeatureCollection(schema); final AffineTransformation jtsTransformation = new AffineTransformation( mt2D.getScaleX(), mt2D.getShearX(), mt2D.getTranslateX(), mt2D.getShearY(), mt2D.getScaleY(), mt2D.getTranslateY()); for (LineString line : prop) { // get value Double value = (Double) line.getUserData(); line.setUserData(null); // filter coordinates in place line.apply(jtsTransformation); // create feature and add to list builder.set("the_geom", line); builder.set("value", value); featureCollection.add(builder.buildFeature(String.valueOf(i++))); } // return value return featureCollection; }