001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2006-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.RenderingHints;
019: import java.awt.geom.AffineTransform;
020: import java.awt.geom.NoninvertibleTransformException;
021: import java.awt.geom.Point2D;
022: import java.awt.geom.Rectangle2D;
023: import java.awt.image.RenderedImage;
024: import java.awt.image.renderable.ParameterBlock;
025: import java.util.ArrayList;
026: import java.util.Collections;
027: import java.util.List;
028:
029: import javax.media.jai.ImageLayout;
030: import javax.media.jai.Interpolation;
031: import javax.media.jai.JAI;
032: import javax.media.jai.PlanarImage;
033: import javax.media.jai.ROI;
034: import javax.media.jai.ROIShape;
035: import javax.media.jai.operator.MosaicDescriptor;
036:
037: import org.geotools.coverage.GridSampleDimension;
038: import org.geotools.coverage.grid.GeneralGridRange;
039: import org.geotools.coverage.grid.GridCoverage2D;
040: import org.geotools.coverage.grid.GridGeometry2D;
041: import org.geotools.coverage.processing.CannotCropException;
042: import org.geotools.coverage.processing.OperationJAI;
043: import org.geotools.factory.GeoTools;
044: import org.geotools.factory.Hints;
045: import org.geotools.geometry.Envelope2D;
046: import org.geotools.geometry.GeneralEnvelope;
047: import org.geotools.geometry.jts.ReferencedEnvelope;
048: import org.geotools.referencing.operation.matrix.XAffineTransform;
049: import org.geotools.referencing.operation.transform.ProjectiveTransform;
050: import org.geotools.resources.coverage.CoverageUtilities;
051: import org.geotools.resources.coverage.FeatureUtilities;
052: import org.geotools.resources.geometry.XRectangle2D;
053: import org.geotools.resources.i18n.ErrorKeys;
054: import org.geotools.resources.i18n.Errors;
055: import org.geotools.resources.image.ImageUtilities;
056: import org.opengis.coverage.grid.GridCoverage;
057: import org.opengis.coverage.grid.GridRange;
058: import org.opengis.metadata.spatial.PixelOrientation;
059: import org.opengis.parameter.ParameterValueGroup;
060: import org.opengis.referencing.operation.TransformException;
061: import org.opengis.util.InternationalString;
062:
063: import com.vividsolutions.jts.geom.Coordinate;
064: import com.vividsolutions.jts.geom.GeometryFactory;
065: import com.vividsolutions.jts.geom.LinearRing;
066: import com.vividsolutions.jts.geom.Polygon;
067: import com.vividsolutions.jts.geom.PrecisionModel;
068:
069: /**
070: * This class is responsible for applying a crop operation to a source coverage
071: * with a specified envelope.
072: *
073: * @author Simone Giannecchini, GeoSolutions
074: *
075: */
076: final class CroppedCoverage2D extends GridCoverage2D {
077: /**
078: *
079: */
080: private static final long serialVersionUID = -501742139906901754L;
081:
082: private final static PrecisionModel pm;
083:
084: private final static GeometryFactory gf;
085: static {
086: // getting default hints
087: final Hints defaultHints = GeoTools.getDefaultHints();
088:
089: // check if someone asked us to use a specific precision model
090: final Object o = defaultHints.get(Hints.JTS_PRECISION_MODEL);
091: if (o != null)
092: pm = (PrecisionModel) o;
093: else {
094: pm = new PrecisionModel();
095: }
096: gf = new GeometryFactory(pm, 0);
097:
098: }
099:
100: /**
101: * Convenience constructor for a {@link CroppedCoverage2D}.
102: *
103: * @param name
104: * for this {@link GridCoverage2D}.
105: * @param sourceRaster
106: * is the raster that will be the back-end for this
107: * {@link GridCoverage2D}.
108: * @param croppedGeometry
109: * is the {@link GridGeometry2D} for this new
110: * {@link CroppedCoverage2D}.
111: * @param source
112: * is the original {@link GridCoverage2D}.
113: * @param actionTaken
114: * it is used to do the necessary postprocessing for supporting
115: * paletted images.
116: * @param rasterSpaceROI
117: * in case we used the JAI's mosaic with a ROI this
118: * {@link java.awt.Polygon} will hold the used roi.
119: */
120: private CroppedCoverage2D(InternationalString name,
121: PlanarImage sourceRaster, GridGeometry2D croppedGeometry,
122: GridCoverage2D source, int actionTaken,
123: java.awt.Polygon rasterSpaceROI) {
124: super (name.toString(), sourceRaster, croppedGeometry,
125: (GridSampleDimension[]) (actionTaken == 1 ? null
126: : source.getSampleDimensions().clone()),
127: new GridCoverage[] { source },
128: rasterSpaceROI != null ? Collections.singletonMap(
129: "GC_ROI", rasterSpaceROI) : null);
130: }
131:
132: /**
133: * Applies the band select operation to a grid coverage.
134: *
135: * @param parameters
136: * List of name value pairs for the parameters.
137: * @param sourceCoverage
138: * is the source {@link GridCoverage2D} that we want to crop.
139: * @param hints
140: * A set of rendering hints, or {@code null} if none.
141: * @param sourceGridToWorldTransform
142: * is the 2d grid-to-world transform for the source coverage.
143: * @param scaleFactor
144: * for the grid-to-world transform.
145: * @return The result as a grid coverage.
146: */
147: static GridCoverage2D create(final ParameterValueGroup parameters,
148: RenderingHints hints, GridCoverage2D sourceCoverage,
149: AffineTransform sourceGridToWorldTransform,
150: double scaleFactor) {
151:
152: // /////////////////////////////////////////////////////////////////////
153: //
154: // Getting the source coverage and its child geolocation objects
155: //
156: // /////////////////////////////////////////////////////////////////////
157: final RenderedImage sourceImage = sourceCoverage
158: .getRenderedImage();
159: final Envelope2D sourceEnvelope = sourceCoverage
160: .getEnvelope2D();
161: final GridGeometry2D sourceGridGeometry = ((GridGeometry2D) sourceCoverage
162: .getGridGeometry());
163: final GeneralGridRange sourceGridRange = (GeneralGridRange) sourceGridGeometry
164: .getGridRange();
165: final double rotationEsteem = XAffineTransform
166: .getRotation(sourceGridToWorldTransform);
167:
168: // /////////////////////////////////////////////////////////////////////
169: //
170: // Now we try to understand if we have a simple scale and translate or a
171: // more elaborated grid-to-world transformation n which case a simple
172: // crop could not be enough, but we may need a more elaborated chain of
173: // operation in order to do a good job.
174: //
175: // /////////////////////////////////////////////////////////////////////
176: boolean isSimpleTransform = false;
177: // check if there is a valid rotation value (it could be 0!)
178: if (!Double.isNaN(rotationEsteem)) {
179: //XXX make this parametric somehow
180: if (Math.abs(rotationEsteem) < 1E-3)
181: // there is no rotation
182: isSimpleTransform = true;
183: }
184:
185: // /////////////////////////////////////////////////////////////////////
186: //
187: // Managing Hints, especially for output coverage's layout purposes
188: //
189: // /////////////////////////////////////////////////////////////////////
190: RenderingHints targetHints = ImageUtilities
191: .getRenderingHints(sourceImage);
192: if (targetHints == null) {
193: targetHints = new RenderingHints(hints);
194: } else if (hints != null) {
195: targetHints.add(hints);
196: }
197: // /////////////////////////////////////////////////////////////////////
198: //
199: // Interpolation
200: //
201: // /////////////////////////////////////////////////////////////////////
202: Interpolation interpolation = (Interpolation) targetHints
203: .get(JAI.KEY_INTERPOLATION);
204: if (interpolation == null)
205: interpolation = (Interpolation) ImageUtilities.NN_INTERPOLATION_HINT
206: .get(JAI.KEY_INTERPOLATION);
207:
208: // /////////////////////////////////////////////////////////////////////
209: //
210: // Do we need to explode the Palette to RGB(A)?
211: //
212: // /////////////////////////////////////////////////////////////////////
213: int actionTaken = 0;
214:
215: // //
216: //
217: // Layout
218: //
219: // //
220: ImageLayout layout = (ImageLayout) targetHints
221: .get(JAI.KEY_IMAGE_LAYOUT);
222: if (layout != null) {
223: layout = (ImageLayout) layout.clone();
224: } else {
225: layout = new ImageLayout(sourceImage);
226: layout.unsetTileLayout();
227: // At this point, only the color model and sample model are left
228: // valids.
229: }
230: // crop will ignore minx, miny width and height
231: if ((layout.getValidMask() & (ImageLayout.TILE_WIDTH_MASK
232: | ImageLayout.TILE_HEIGHT_MASK
233: | ImageLayout.TILE_GRID_X_OFFSET_MASK | ImageLayout.TILE_GRID_Y_OFFSET_MASK)) == 0) {
234: layout.setTileGridXOffset(layout.getMinX(sourceImage));
235: layout.setTileGridYOffset(layout.getMinY(sourceImage));
236: final int width = layout.getWidth(sourceImage);
237: final int height = layout.getHeight(sourceImage);
238: if (layout.getTileWidth(sourceImage) > width)
239: layout.setTileWidth(width);
240: if (layout.getTileHeight(sourceImage) > height)
241: layout.setTileHeight(height);
242: }
243: targetHints.put(JAI.KEY_IMAGE_LAYOUT, layout);
244:
245: // /////////////////////////////////////////////////////////////////////
246: //
247: // prepare the processor to use for this operation
248: //
249: // /////////////////////////////////////////////////////////////////////
250: final JAI processor = OperationJAI.getJAI(targetHints);
251: final boolean useProvidedProcessor = !processor.equals(JAI
252: .getDefaultInstance());
253:
254: try {
255: // /////////////////////////////////////////////////////////////////////
256: //
257: // Get the crop envelope and do your thing!
258: //
259: // /////////////////////////////////////////////////////////////////////
260: final GeneralEnvelope cropEnvelope = (GeneralEnvelope) parameters
261: .parameter("Envelope").getValue();
262: // should we conserve the crop envelope?
263: final Boolean conserveEnvelope = (Boolean) parameters
264: .parameter("ConserveEnvelope").getValue();
265:
266: // ////////////////////////////////////////////////////////////////////
267: //
268: // Do we actually need to crop?
269: //
270: // If the intersecton envelope is empty or if the intersection
271: // envelope is (almost) the same of the original envelope we just
272: // return (with different return values).
273: //
274: // ////////////////////////////////////////////////////////////////////
275: if (cropEnvelope.isEmpty())
276: throw new CannotCropException(Errors
277: .format(ErrorKeys.CANT_CROP));
278: if (cropEnvelope.equals(sourceEnvelope, scaleFactor / 2.0,
279: false))
280: return sourceCoverage;
281:
282: // //
283: //
284: // build the new range by keeping into
285: // account translation of grid geometry constructor for respecting
286: // OGC PIXEL-IS-CENTER ImageDatum assumption.
287: //
288: // //
289: final AffineTransform sourceWorldToGridTransform = sourceGridToWorldTransform
290: .createInverse();
291: // finalGridRange will hold the rectangular crop area at the end of
292: // this operation
293: final Rectangle2D finalGridRange = XAffineTransform
294: .transform(sourceWorldToGridTransform, cropEnvelope
295: .toRectangle2D(), null);
296: // intersection with the original range in order to not try to crop
297: // outside the image bounds
298: XRectangle2D.intersect(finalGridRange, sourceGridRange
299: .toRectangle(), finalGridRange);
300:
301: // ////////////////////////////////////////////////////////////////////
302: //
303: //
304: // It is worth to point out that doing a crop the G2W transform
305: // should not change while the envelope might change a little bit as
306: // a consequence of the roundings of the underlying image datum
307: // which uses integer factors. This can be avoided using the
308: // conserveEnvelope param. If the user does not explicitly asks to
309: // conserve the crop envelope we will conserve the original
310: // grid-to-world transform.
311: //
312: // ////////////////////////////////////////////////////////////////////
313: final GeneralGridRange newRange = new GeneralGridRange(
314: new GeneralEnvelope(finalGridRange));
315: // we do not have to crop in this case (should not really happen at
316: // this time)
317: if (newRange.equals(sourceGridRange) && isSimpleTransform)
318: return sourceCoverage;
319:
320: // ////////////////////////////////////////////////////////////////////
321: //
322: // if I get here I have something to crop
323: // using the world-to-grid transform for going from envelope to the
324: // new grid range.
325: //
326: // ////////////////////////////////////////////////////////////////////
327: final int xAxis = sourceGridGeometry.gridDimensionX;
328: final int yAxis = sourceGridGeometry.gridDimensionY;
329: final double minX = newRange.getLower(xAxis);
330: final double minY = newRange.getLower(yAxis);
331: final double width = newRange.getLength(xAxis);
332: final double height = newRange.getLength(yAxis);
333: assert width > 0;
334: assert height > 0;
335:
336: // /////////////////////////////////////////////////////////////////////
337: //
338: // get the rendered image and crop it
339: //
340: // /////////////////////////////////////////////////////////////////////
341: final PlanarImage croppedImage;
342: final ParameterBlock pbj = new ParameterBlock();
343: pbj.addSource(sourceImage);
344: java.awt.Polygon rasterSpaceROI = null;
345: if (isSimpleTransform) {
346:
347: // /////////////////////////////////////////////////////////////////////
348: //
349: // We got a simple scale and translate transform, JAI crop will
350: // suffice.
351: //
352: // /////////////////////////////////////////////////////////////////////
353: // executing the crop
354: pbj.add(new Float(minX));
355: pbj.add(new Float(minY));
356: pbj.add(new Float(width));
357: pbj.add(new Float(height));
358: if (!useProvidedProcessor)
359: croppedImage = JAI.create("Crop", pbj, targetHints);
360: else
361: croppedImage = processor.createNS("Crop", pbj,
362: targetHints);
363: } else {
364: // /////////////////////////////////////////////////////////////////////
365: //
366: // We don't have a simple scale and translate transform, JAI
367: // crop MAY NOT suffice. Let's decide whether or not we'll use
368: // the Mosaic.
369: //
370: // /////////////////////////////////////////////////////////////////////
371: // //
372: //
373: // Convert the crop envelope into a polygon and the use the
374: // world-to-grid transform to get a ROI for the source coverage.
375: //
376: // //
377: final Rectangle2D rect = cropEnvelope.toRectangle2D();
378: final Coordinate[] coord = new Coordinate[] {
379: new Coordinate(rect.getMinX(), rect.getMinY()),
380: new Coordinate(rect.getMinX(), rect.getMaxY()),
381: new Coordinate(rect.getMaxX(), rect.getMaxY()),
382: new Coordinate(rect.getMaxX(), rect.getMinY()),
383: new Coordinate(rect.getMinX(), rect.getMinY()) };
384: final LinearRing ring = gf.createLinearRing(coord);
385: final Polygon modelSpaceROI = new Polygon(ring, null,
386: gf);
387:
388: // check that we have the same thing here
389: assert modelSpaceROI.getEnvelopeInternal().equals(
390: new ReferencedEnvelope(rect, cropEnvelope
391: .getCoordinateReferenceSystem()));
392: // //
393: //
394: // Now convert this polygon back into a shape for the source
395: // raster space.
396: //
397: // //
398: final List points = new ArrayList(5);
399: rasterSpaceROI = FeatureUtilities
400: .convertPolygonToPointArray(
401: modelSpaceROI,
402: ProjectiveTransform
403: .create(sourceWorldToGridTransform),
404: points);
405:
406: final double cropArea = ((GeneralGridRange) newRange)
407: .toRectangle().width
408: * ((GeneralGridRange) newRange).toRectangle().height;
409: final double roiArea = Math.abs(area((Point2D[]) points
410: .toArray(new Point2D[] {})));
411: final double roiOpt = parameters.parameter(
412: "ROITolerance").doubleValue();
413: final String operatioName;
414: if (roiOpt * cropArea > roiArea) {
415: // executing the mosaic
416: final ROIShape roi = new ROIShape(rasterSpaceROI);
417: pbj.add(MosaicDescriptor.MOSAIC_TYPE_OVERLAY);
418: pbj.add(null);
419: pbj.add(new ROI[] { roi });
420: pbj.add(null);
421: pbj.add(CoverageUtilities
422: .getBackgroundValues(sourceCoverage));
423: // nice trick, we use the layout to do the actual crop
424: Rectangle2D roiBounds = roi.getBounds2D();
425: XRectangle2D.intersect(roiBounds, sourceGridRange
426: .toRectangle(), roiBounds);
427: layout.setMinX((int) (roiBounds.getMinX() + 0.5));
428: layout.setWidth((int) (roiBounds.getWidth() + 0.5));
429: layout.setMinY((int) (roiBounds.getMinY() + 0.5));
430: layout
431: .setHeight((int) (roiBounds.getHeight() + 0.5));
432:
433: operatioName = "Mosaic";
434: } else {
435: // executing the crop
436: pbj.add(new Float(minX));
437: pbj.add(new Float(minY));
438: pbj.add(new Float(width));
439: pbj.add(new Float(height));
440: operatioName = "Crop";
441: }
442:
443: if (!useProvidedProcessor)
444: croppedImage = JAI.create(operatioName, pbj,
445: targetHints);
446: else
447: croppedImage = processor.createNS(operatioName,
448: pbj, targetHints);
449: }
450: if (conserveEnvelope.booleanValue())
451: return new CroppedCoverage2D(sourceCoverage.getName(),
452: croppedImage, new GridGeometry2D(
453: new GeneralGridRange(croppedImage),
454: cropEnvelope), sourceCoverage,
455: actionTaken, rasterSpaceROI);
456: else
457: return new CroppedCoverage2D(
458: sourceCoverage.getName(),
459: croppedImage,
460: new GridGeometry2D(
461: new GeneralGridRange(croppedImage),
462: sourceGridGeometry
463: .getGridToCRS2D(PixelOrientation.CENTER),
464: sourceCoverage
465: .getCoordinateReferenceSystem()),
466: sourceCoverage, actionTaken, rasterSpaceROI);
467:
468: } catch (TransformException e) {
469: throw new CannotCropException(Errors
470: .format(ErrorKeys.CANT_CROP), e);
471: } catch (NoninvertibleTransformException e) {
472: throw new CannotCropException(Errors
473: .format(ErrorKeys.CANT_CROP), e);
474: }
475:
476: }
477:
478: /**
479: * Function to calculate the area of a polygon, according to the algorithm
480: * defined at http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/
481: *
482: * @param polyPoints
483: * array of points in the polygon
484: * @return area of the polygon defined by pgPoints
485: */
486: private static double area(Point2D[] polyPoints) {
487: int i, j, n = polyPoints.length;
488: double area = 0;
489:
490: for (i = 0; i < n; i++) {
491: j = (i + 1) % n;
492: area += polyPoints[i].getX() * polyPoints[j].getY();
493: area -= polyPoints[j].getX() * polyPoints[i].getY();
494: }
495: area /= 2.0;
496: return (area);
497: }
498: }
|