/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006-2008, Open Source Geospatial Foundation (OSGeo) * * 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.coverage.processing.operation; import java.awt.Polygon; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.processing.CannotCropException; import org.geotools.coverage.processing.Operation2D; import org.geotools.factory.Hints; import org.geotools.geometry.Envelope2D; import org.geotools.geometry.GeneralEnvelope; import org.geotools.metadata.iso.citation.Citations; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.parameter.DefaultParameterDescriptorGroup; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.opengis.coverage.Coverage; import org.opengis.geometry.Envelope; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; /** * The crop operation is responsible for selecting geographic subarea of the * source coverage. The CoverageCrop operation does not merely wrap the JAI Crop * operation but it goes beyond that as far as capabilities. * * <p> * The key point is that the CoverageCrop operation aims to perform a spatial * crop, i.e. cropping the underlying raster by providing a spatial * {@link Envelope} (if the envelope is not 2D only the 2D part of it will be * used). This means that, depending on the grid-to-world transformation * existing for the raster we want to crop, the crop area in the raster space * might not be a rectangle, hence JAI's crop may not suffice in order to shrink * the raster area we would obtain. For this purpose this operation make use of * either the JAI's Crop or Mosaic operations depending on the conditions in * which we are working. * * * <p> * <strong>Meaning of the ROI_OPTIMISATION_TOLERANCE parameter</strong> <br> * In general if the grid-to-world transform is a simple scale and translate * using JAI's crop should suffice, but when the g2w transform contains * rotations or skew then we need something more elaborate since a rectangle in * model space may not map to a rectangle in raster space. We would still be * able to crop using JAI's crop on this polygon bounds but, depending on how * this rectangle is built, we would be highly inefficient. In order to overcome * this problems we use a combination of JAI's crop and mosaic since the mosaic * can be used to crop a raster using a general ROI instead of a simple * rectangle. There is a negative effect though. Crop would not create a new * raster but simply forwards requests back to the original one (it basically * create a viewport on the source raster) while the mosaic operation creates a * new raster. We try to address this trade-off by providing the parameter * {@link Crop#ROI_OPTIMISATION_TOLERANCE}, which basically tells this * operation "Use the mosaic operation only if the area that we would load with * the Mosaic is strictly smaller then (ROI_OPTIMISATION_TOLERANCE)* A' where A' * is the area of the polygon resulting from converting the crop area from the * model space to the raster space. * * <p> * <strong>Meaning of the CONSERVE_ENVELOPE parameter</strong> <br> * Deprecated. * * <p> * <strong>NOTE</strong> that in case we will use the Mosaic operation with a * ROI, such a ROI will be added as a synthetic property to the resulting * coverage. The key for this property will be GC_ROI and the type of the object * {@link Polygon}. * * @source $URL$ * @todo make this operation work with a general polygon. instead of an envelope. * @todo make this operation more t,z friendly * @version $Id$ * @author Simone Giannecchini * @since 2.3 * * @see javax.media.jai.operator.ScaleDescriptor */ public class Crop extends Operation2D { /** * Serial number for cross-version compatibility. */ private static final long serialVersionUID = 4466072819239413456L; /** * The parameter descriptor used to pass this operation the envelope to use * when doing the spatial crop. */ public static final ParameterDescriptor<Envelope> CROP_ENVELOPE = new DefaultParameterDescriptor<Envelope>( Citations.GEOTOOLS, "Envelope", Envelope.class, // Value class null, // Array of valid values null, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure false); // Parameter is optional /** * The parameter descriptor used to tell this operation to optimize the crop * using a Mosaic in where the area of the image we would not load is smaller * than ROI_OPTIMISATION_TOLERANCE*FULL_CROP. */ public static final ParameterDescriptor<Double> ROI_OPTIMISATION_TOLERANCE = new DefaultParameterDescriptor<Double>( Citations.GEOTOOLS, "ROITolerance", Double.class, // Value class null, // Array of valid values 0.6, // Default value 0.0, // Minimal value 1.0, // Maximal value null, // Unit of measure true); // Parameter is optional /** * The parameter descriptor is basically a simple boolean that tells * this operation to try to conserve the envelope that it gets as input. * * <p> * <strong> Note that this might mean obtaining a coverage whose grid to * world 2D has been slightly changed to account for the rounding which * is applied in order to get integer coordinate for the raster to crop. * </strong> * <p> * * See this class javadocs for an explanation. * @deprecated */ public static final ParameterDescriptor<Boolean> CONSERVE_ENVELOPE = new DefaultParameterDescriptor<Boolean>( Citations.GEOTOOLS, "ConserveEnvelope", Boolean.class, // Value // class new Boolean[]{Boolean.TRUE,Boolean.FALSE}, // Array of valid values Boolean.FALSE, // Default value null, // Minimal value null, // Maximal value null, // Unit of measure true); // Parameter is optional /** * Constructs a default {@code "Crop"} operation. */ public Crop() { super(new DefaultParameterDescriptorGroup(Citations.GEOTOOLS, "CoverageCrop", new ParameterDescriptor[] { SOURCE_0, CROP_ENVELOPE, CONSERVE_ENVELOPE, ROI_OPTIMISATION_TOLERANCE })); } /** * Applies a crop operation to a coverage. * * @see org.geotools.coverage.processing.AbstractOperation#doOperation(org.opengis.parameter.ParameterValueGroup, * org.geotools.factory.Hints) */ public Coverage doOperation(ParameterValueGroup parameters, Hints hints) { // ///////////////////////////////////////////////////////////////////// // // Checking input parameters // // /////////////////////////////////////////////////////////////////// // source coverage final ParameterValue sourceParameter = parameters.parameter("Source"); if (sourceParameter == null || !(sourceParameter.getValue() instanceof GridCoverage2D)) { throw new CannotCropException(Errors.format( ErrorKeys.NULL_PARAMETER_$2, "Source", GridCoverage2D.class .toString())); } // crop envelope final ParameterValue envelopeParameter = parameters.parameter("Envelope"); if (envelopeParameter == null || !(envelopeParameter.getValue() instanceof Envelope)) throw new CannotCropException(Errors.format(ErrorKeys.NULL_PARAMETER_$2, "Envelope",GeneralEnvelope.class.toString())); // should we conserve the crop envelope final ParameterValue conserveEnvelopeParameter = parameters.parameter("ConserveEnvelope"); if (conserveEnvelopeParameter == null|| !(conserveEnvelopeParameter.getValue() instanceof Boolean)) throw new CannotCropException(Errors.format(ErrorKeys.NULL_PARAMETER_$2, "ConserveEnvelope",Double.class.toString())); // ///////////////////////////////////////////////////////////////////// // // Initialization // // We take the crop envelope and the source envelope then we check their // crs and we also check if they ever overlap. // // ///////////////////////////////////////////////////////////////////// final GridCoverage2D source = (GridCoverage2D) sourceParameter .getValue(); // envelope of the source coverage final Envelope2D sourceEnvelope = source.getEnvelope2D(); // crop envelope Envelope2D destinationEnvelope = new Envelope2D( (Envelope) envelopeParameter.getValue()); CoordinateReferenceSystem sourceCRS, destinationCRS; sourceCRS = sourceEnvelope.getCoordinateReferenceSystem(); destinationCRS = destinationEnvelope.getCoordinateReferenceSystem(); if (destinationCRS == null) { // Do not change the user provided object - clone it first. final Envelope2D ge = new Envelope2D(destinationEnvelope); destinationCRS = source.getCoordinateReferenceSystem2D(); ge.setCoordinateReferenceSystem(destinationCRS); destinationEnvelope = ge; } // // // // Source and destination crs must to be equals // // // if (!CRS.equalsIgnoreMetadata(sourceCRS, destinationCRS)) { throw new CannotCropException(Errors.format(ErrorKeys.MISMATCHED_ENVELOPE_CRS_$2, sourceCRS.getName().getCode(), destinationCRS.getName().getCode())); } // // // // Check the intersection and, if needed, do the crop operation. // // // final GeneralEnvelope intersectionEnvelope = new GeneralEnvelope((Envelope) destinationEnvelope); intersectionEnvelope.setCoordinateReferenceSystem(source.getCoordinateReferenceSystem()); // intersect the envelopes intersectionEnvelope.intersect(sourceEnvelope); if (intersectionEnvelope.isEmpty()) throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP)); // // // // Get the grid-to-world transform by keeping into account translation // of grid geometry constructor for respecting OGC PIXEL-IS-CENTER // ImageDatum assumption. // // // final AffineTransform sourceGridToWorld = (AffineTransform) ((GridGeometry2D) source.getGridGeometry()).getGridToCRS2D(PixelOrientation.UPPER_LEFT); // // // // I set the tolerance as half the scale factor of the grid-to-world // transform. This should more or less means in most cases "don't bother // to crop if the new envelope is as close to the old one that we go // deep under pixel size." // // // final double tolerance = XAffineTransform.getScale(sourceGridToWorld); if (!intersectionEnvelope.equals(sourceEnvelope, tolerance / 2.0, false)) { envelopeParameter.setValue(intersectionEnvelope.clone()); return CroppedCoverage2D.create(parameters,(hints instanceof Hints) ? (Hints) hints: new Hints(hints), source,sourceGridToWorld, tolerance); } else { // // // // Note that in case we don't crop at all, WE DO NOT UPDATE the // envelope. If we did we might end up doing multiple successive // crop without actually cropping the image but, still, we would // shrink the envelope each time. Just think about having a loop // that crops recursively the same coverage specifying each time an // envelope whose URC is only a a scale quarter close to the LLC of // the old one. We would never crop the raster but we would modify // the grid-to-world transform each time. // // // return source; } } /** * Function to calculate the area of a polygon, according to the algorithm * defined at http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ * * @param polyPoints * array of points in the polygon * @return area of the polygon defined by pgPoints */ static double area(Point2D[] polyPoints) { int i, j, n = polyPoints.length; double area = 0; for (i = 0; i < n; i++) { j = (i + 1) % n; area += polyPoints[i].getX() * polyPoints[j].getY(); area -= polyPoints[j].getX() * polyPoints[i].getY(); } area /= 2.0; return (area); } // /** // * In order to conserve the original envelope we used for this request // * we have to slightly correct the original grid to world transform in // * order to take into account the fact that we snapped the underlying // * raster to the integer grid. This would involve a "scale and // * translate" transformation which we are here accounting for. // * // * @param cornerGridToWorld // * original grid to world transform referred to the // * corner of the cells in raster space. // * @param minX minimum x coordinate for the cropped raster in integer raster space. // * @param minY minimum y coordinate for the cropped raster in integer raster space. // * @param width width for the cropped raster in integer raster space. // * @param height height for the cropped raster in integer raster space. // * @param floatingPointRange range of the cropped raster in floating point raster space. // * @return a {@link MathTransform} which conserves the original envelope. // */ // static MathTransform createCorrectedTranform( // final AffineTransform cornerGridToWorld, // final double minX, // final double minY, // final double width, // final double height, // final Rectangle2D floatingPointRange) { // // // computing the factor for the corrections affine transform to map // // from the integer raster space to the floating point raster space. // final double scaleX=floatingPointRange.getWidth()/width; // final double scaleY=floatingPointRange.getHeight()/height; // final double tx=floatingPointRange.getMinX()-minX*scaleX; // final double ty=floatingPointRange.getMinY()-minY*scaleY; // final AffineTransform translationTransform= new AffineTransform(scaleX,0,0,scaleY,tx,ty); // // //now correct the original grid to world transform // translationTransform.preConcatenate(cornerGridToWorld); // return ProjectiveTransform.create(translationTransform); // // } }