001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2006, Geotools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation; either
009: * version 2.1 of the License, or (at your option) any later version.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.coverage.processing.operation;
017:
018: import java.awt.Polygon;
019: import java.awt.geom.AffineTransform;
020:
021: import org.geotools.coverage.grid.GridCoverage2D;
022: import org.geotools.coverage.grid.GridGeometry2D;
023: import org.geotools.coverage.processing.CannotCropException;
024: import org.geotools.coverage.processing.Operation2D;
025: import org.geotools.factory.Hints;
026: import org.geotools.geometry.Envelope2D;
027: import org.geotools.geometry.GeneralEnvelope;
028: import org.geotools.metadata.iso.citation.Citations;
029: import org.geotools.parameter.DefaultParameterDescriptor;
030: import org.geotools.parameter.DefaultParameterDescriptorGroup;
031: import org.geotools.referencing.CRS;
032: import org.geotools.referencing.operation.matrix.XAffineTransform;
033: import org.geotools.resources.i18n.ErrorKeys;
034: import org.geotools.resources.i18n.Errors;
035: import org.opengis.coverage.Coverage;
036: import org.opengis.coverage.processing.OperationNotFoundException;
037: import org.opengis.geometry.Envelope;
038: import org.opengis.metadata.spatial.PixelOrientation;
039: import org.opengis.parameter.ParameterDescriptor;
040: import org.opengis.parameter.ParameterValue;
041: import org.opengis.parameter.ParameterValueGroup;
042: import org.opengis.referencing.crs.CoordinateReferenceSystem;
043:
044: /**
045: * The crop operation is responsible for selecting geographic subarea of the
046: * source coverage. The CoverageCrop operation does not merely wrap the JAI Crop
047: * operation but it goes beyond that as far as capabilities.
048: *
049: * <p>
050: * The key point is that the CoverageCrop operation aims to perform a spatial
051: * crop, i.e. cropping the underlying raster by providing a spatial
052: * {@link Envelope} (if the envelope is not 2D only the 2D part of it will be
053: * used). This means that, depending on the grid-to-world transformation
054: * existing for the raster we want to crop the crop area in the raster space
055: * might not be a rectangle, hence JAI's crop may not suffice in order to shrink
056: * the raster area we would obtain. For this purpose this operation make use of
057: * either the JAI's Crop or Mosaic operations depending on the conditions in
058: * which we are working.
059: *
060: *
061: * <p>
062: * <strong>Meaning of the ROI_OPTIMISATION_TOLERANCE parameter</strong> <br>
063: * In general if the grid-to-world transform is a simple scale and translate
064: * using JAI's crop should suffice, but when the g2w transform contains
065: * rotations or skew then we need something more elaborate since a rectangle in
066: * model space may not map to a rectangle in raster space. We would still be
067: * able to crop using JAI's crop on this polygon bounds but, depending on how
068: * this rectangle is built, we would be highly inefficient. In order to overcome
069: * this problems we use a combination of JAI's crop and mosaic since the mosaic
070: * can be used to crop a raster using a general ROI instead of a simple
071: * rectangle. There is a negative effect though. Crop would not create a new
072: * raster but simply forwards requests back to the origina one (it basically
073: * create a viewport on the source raster) while the mosaic operation creates a
074: * new raster. We try to address this trade-off by providing the parameter
075: * {@link Crop#ROI_OPTIMISATION_TOLERANCE}, which basically tells this
076: * operation "Use the mosaic operation only if the area that we would load with
077: * the Mosaic is strictly smaller then (ROI_OPTIMISATION_TOLERANCE)* A' where A'
078: * is the area of the polygon resulting from converting the crop area from the
079: * model space to the raster space.
080: *
081: * <p>
082: * <strong>Meaning of the CONSERVE_ENVELOPE parameter</strong> <br>
083: * When we crop a coverage using a spatial envelope we may incur in a few issues
084: * with approximations when applying the grid-to-world transform and its
085: * inverse. Goal of this parameter is to suggest this operation to conserve the
086: * input crop envelope, if possible, instead of conserving the original
087: * grid-to-world transform. This would help when doing something like building a
088: * mosaic from a single coverage.
089: *
090: * <p>
091: * <strong>NOTE</strong> that in case we will use the Mosaic operation with a
092: * ROI, such a ROI will be added as a synthetic property to the resulting
093: * coverage. The key for this property will be GC_ROI and the type of the object
094: * {@link Polygon}.
095: *
096: * @todo make this operation work with a general polygon. instead of an
097: * envelope.
098: * @todo make this operation work when having a shear
099: * @todo make the tolerance for rotations parametric
100: * @source $URL:
101: * http://svn.geotools.org/geotools/branches/2.4.x_rs/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Crop.java $
102: * @version $Id: Crop.java 27037 2007-09-18 12:58:10Z simboss $
103: * @author Simone Giannecchini, GeoSolutions
104: * @since 2.3
105: *
106: * @see javax.media.jai.operator.ScaleDescriptor
107: */
108: public class Crop extends Operation2D {
109: /**
110: * Serial number for cross-version compatibility.
111: */
112: private static final long serialVersionUID = 4466072819239413456L;
113:
114: /**
115: * The parameter descriptor used to pass this operation the envelope to use
116: * when doing the spatial crop.
117: */
118: public static final ParameterDescriptor CROP_ENVELOPE = new DefaultParameterDescriptor(
119: Citations.GEOTOOLS, "Envelope", Envelope.class, // Value
120: // class
121: null, // Array of valid values
122: null, // Default value
123: null, // Minimal value
124: null, // Maximal value
125: null, // Unit of measure
126: false); // Parameter is optional
127:
128: /**
129: * The parameter descriptor use to tell this operation to optimize the crop
130: * using a Mosaic in where the are of the image we would not load is smaller
131: * than ROI_OPTIMISATION_TOLERANCE*FULL_CROP.
132: */
133: public static final ParameterDescriptor ROI_OPTIMISATION_TOLERANCE = new DefaultParameterDescriptor(
134: Citations.GEOTOOLS, "ROITolerance", Double.class, // Value class
135: null, // Array of valid values
136: new Double(0.6), // Default value
137: new Double(0), // Minimal value
138: new Double(1.0), // Maximal value
139: null, // Unit of measure
140: true); // Parameter is optional
141:
142: /**
143: * The parameter descriptor is basically a simple boolean that tells this
144: * operation to conserve the envelope that it gets as input.
145: *
146: * <p>
147: * See this class javadocs for an explanation.
148: */
149: public static final ParameterDescriptor CONSERVE_ENVELOPE = new DefaultParameterDescriptor(
150: Citations.GEOTOOLS, "ConserveEnvelope", Boolean.class, // Value
151: // class
152: new Boolean[] { Boolean.TRUE, Boolean.FALSE }, // Array of valid
153: // values
154: Boolean.FALSE, // Default value
155: null, // Minimal value
156: null, // Maximal value
157: null, // Unit of measure
158: true); // Parameter is optional
159:
160: /**
161: *
162: * @throws OperationNotFoundException
163: */
164: public Crop() {
165: super (new DefaultParameterDescriptorGroup(Citations.GEOTOOLS,
166: "CoverageCrop", new ParameterDescriptor[] { SOURCE_0,
167: CROP_ENVELOPE, CONSERVE_ENVELOPE,
168: ROI_OPTIMISATION_TOLERANCE }));
169:
170: }
171:
172: /*
173: * (non-Javadoc)
174: *
175: * @see org.geotools.coverage.processing.AbstractOperation#doOperation(org.opengis.parameter.ParameterValueGroup,
176: * org.geotools.factory.Hints)
177: */
178: public Coverage doOperation(ParameterValueGroup parameters,
179: Hints hints) {
180: // /////////////////////////////////////////////////////////////////////
181: //
182: // Checking input parameters
183: //
184: // ///////////////////////////////////////////////////////////////////
185: // source coverage
186: final ParameterValue sourceParameter = parameters
187: .parameter("Source");
188: if (sourceParameter == null
189: || !(sourceParameter.getValue() instanceof GridCoverage2D)) {
190: throw new CannotCropException(Errors.format(
191: ErrorKeys.NULL_PARAMETER_$2, "Source",
192: GridCoverage2D.class.toString()));
193: }
194: // crop envelope
195: final ParameterValue envelopeParameter = parameters
196: .parameter("Envelope");
197: if (envelopeParameter == null
198: || !(envelopeParameter.getValue() instanceof Envelope))
199: throw new CannotCropException(Errors.format(
200: ErrorKeys.NULL_PARAMETER_$2, "Envelope",
201: GeneralEnvelope.class.toString()));
202: // should we conserve the crop envelope
203: final ParameterValue conserveEnvelopeParameter = parameters
204: .parameter("ConserveEnvelope");
205: if (conserveEnvelopeParameter == null
206: || !(conserveEnvelopeParameter.getValue() instanceof Boolean))
207: throw new CannotCropException(Errors.format(
208: ErrorKeys.NULL_PARAMETER_$2, "ConserveEnvelope",
209: Double.class.toString()));
210:
211: // /////////////////////////////////////////////////////////////////////
212: //
213: // Initialization
214: //
215: // We take the crop envelope and the source envelope then we check their
216: // crs and we also check if they ever overlap.
217: //
218: // /////////////////////////////////////////////////////////////////////
219: final GridCoverage2D source = (GridCoverage2D) sourceParameter
220: .getValue();
221: // envelope of the source coverage
222: final Envelope2D sourceEnvelope = source.getEnvelope2D();
223: // crop envelope
224: Envelope2D destinationEnvelope = new Envelope2D(
225: (Envelope) envelopeParameter.getValue());
226: CoordinateReferenceSystem sourceCRS, destinationCRS;
227: sourceCRS = sourceEnvelope.getCoordinateReferenceSystem();
228: destinationCRS = destinationEnvelope
229: .getCoordinateReferenceSystem();
230: if (destinationCRS == null) {
231: // Do not change the user provided object - clone it first.
232: final Envelope2D ge = new Envelope2D(destinationEnvelope);
233: destinationCRS = source.getCoordinateReferenceSystem2D();
234: ge.setCoordinateReferenceSystem(destinationCRS);
235: destinationEnvelope = ge;
236: }
237:
238: // //
239: //
240: // Source and destination crs must to be equals
241: //
242: // //
243: if (!CRS.equalsIgnoreMetadata(sourceCRS, destinationCRS)) {
244: throw new CannotCropException(Errors.format(
245: ErrorKeys.MISMATCHED_ENVELOPE_CRS_$2, sourceCRS
246: .getName().getCode(), destinationCRS
247: .getName().getCode()));
248: }
249: // //
250: //
251: // Check the intersection and, if needed, do the crop operation.
252: //
253: // //
254: final GeneralEnvelope intersectionEnvelope = new GeneralEnvelope(
255: (Envelope) destinationEnvelope);
256: intersectionEnvelope.setCoordinateReferenceSystem(source
257: .getCoordinateReferenceSystem());
258: // intersect the envelopes
259: intersectionEnvelope.intersect(sourceEnvelope);
260: if (intersectionEnvelope.isEmpty())
261: throw new CannotCropException(Errors
262: .format(ErrorKeys.CANT_CROP));
263: // //
264: //
265: // Get the grid-to-world transform by keeping into account translation
266: // of grid geometry constructor for respecting OGC PIXEL-IS-CENTER
267: // ImageDatum assumption.
268: //
269: // //
270: final AffineTransform sourceGridToWorld = (AffineTransform) ((GridGeometry2D) source
271: .getGridGeometry())
272: .getGridToCRS2D(PixelOrientation.UPPER_LEFT);
273: // //
274: //
275: // I set the tolerance as half the scale factor of the grid-to-world
276: // transform. This should more or less means in most cases "don't bother
277: // to crop if the new envelope is as close to the old one that we go
278: // deep under pixel size."
279: //
280: // //
281: final double tolerance = XAffineTransform
282: .getScale(sourceGridToWorld);
283: if (!intersectionEnvelope.equals(sourceEnvelope,
284: tolerance / 2.0, false)) {
285: envelopeParameter.setValue(intersectionEnvelope.clone());
286: return CroppedCoverage2D.create(parameters,
287: (hints instanceof Hints) ? (Hints) hints
288: : new Hints(hints), source,
289: sourceGridToWorld, tolerance);
290: } else {
291: // //
292: //
293: // Note that in case we don't crop at all, WE DO NOT UPDATE the
294: // envelope. If we did we might end up doing multiple successive
295: // crop without actually cropping the image but, still, we would
296: // shrink the envelope each time. Just think about having a loop
297: // that crops recursively the same coverage specifying each time an
298: // envelope whose URC is only a a scale quarter close to the LLC of
299: // the old one. We would never crop the raster but we would modify
300: // the grid-to-world trasnform each time.
301: //
302: // //
303: return source;
304: }
305: }
306: }
|