/* * 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.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.RenderedImage; import java.awt.image.renderable.ParameterBlock; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.media.jai.ImageLayout; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import javax.media.jai.operator.MosaicDescriptor; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.processing.CannotCropException; import org.geotools.coverage.processing.OperationJAI; import org.geotools.factory.GeoTools; import org.geotools.factory.Hints; import org.geotools.geometry.Envelope2D; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.image.ImageUtilities; import org.opengis.coverage.grid.GridCoverage; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.parameter.InvalidParameterTypeException; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.operation.TransformException; import org.opengis.util.InternationalString; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.PrecisionModel; /** * This class is responsible for applying a crop operation to a source coverage * with a specified envelope. * * @author Simone Giannecchini, GeoSolutions * */ final class CroppedCoverage2D extends GridCoverage2D { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -501742139906901754L; private final static PrecisionModel pm; private final static GeometryFactory gf; public static final double EPS = 1E-3; static { // getting default hints final Hints defaultHints = GeoTools.getDefaultHints(); // check if someone asked us to use a specific precision model final Object o = defaultHints.get(Hints.JTS_PRECISION_MODEL); if (o != null) pm = (PrecisionModel) o; else { pm = new PrecisionModel(); } gf = new GeometryFactory(pm, 0); } /** * Convenience constructor for a {@link CroppedCoverage2D}. * * @param name * for this {@link GridCoverage2D}. * @param sourceRaster * is the raster that will be the back-end for this * {@link GridCoverage2D}. * @param croppedGeometry * is the {@link GridGeometry2D} for this new * {@link CroppedCoverage2D}. * @param source * is the original {@link GridCoverage2D}. * @param actionTaken * it is used to do the necessary postprocessing for supporting * paletted images. * @param rasterSpaceROI * in case we used the JAI's mosaic with a ROI this * {@link java.awt.Polygon} will hold the used roi. * @param hints * An optional set of hints, or {@code null} if none. */ private CroppedCoverage2D(InternationalString name, PlanarImage sourceRaster, GridGeometry2D croppedGeometry, GridCoverage2D source, int actionTaken, java.awt.Polygon rasterSpaceROI, Hints hints) { super(name.toString(), sourceRaster, croppedGeometry, (GridSampleDimension[]) (actionTaken == 1 ? null : source .getSampleDimensions().clone()), new GridCoverage[] { source }, rasterSpaceROI != null ? Collections.singletonMap("GC_ROI", rasterSpaceROI) : null,hints); } /** * Applies the band select operation to a grid coverage. * * @param parameters * List of name value pairs for the parameters. * @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. * @param scaleFactor * for the grid-to-world transform. * @return The result as a grid coverage. */ static GridCoverage2D create( final ParameterValueGroup parameters, final Hints hints, final GridCoverage2D sourceCoverage, final AffineTransform sourceGridToWorldTransform, final double scaleFactor) { // ///////////////////////////////////////////////////////////////////// // // Getting the source coverage and its child geolocation objects // // ///////////////////////////////////////////////////////////////////// final RenderedImage sourceImage = sourceCoverage.getRenderedImage(); final Envelope2D sourceEnvelope = sourceCoverage.getEnvelope2D(); 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); // ///////////////////////////////////////////////////////////////////// // // Managing Hints, especially for output coverage's layout purposes // // ///////////////////////////////////////////////////////////////////// final RenderingHints targetHints = prepareHints(hints, sourceImage); // ///////////////////////////////////////////////////////////////////// // // Do we need to explode the Palette to RGB(A)? // // ///////////////////////////////////////////////////////////////////// int actionTaken = 0; // // // // Layout // // // 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 { // ///////////////////////////////////////////////////////////////////// // // Get the crop envelope and do your thing! // // ///////////////////////////////////////////////////////////////////// final GeneralEnvelope cropEnvelope = (GeneralEnvelope) parameters.parameter("Envelope").getValue(); // // // // Do we actually need to crop? // // If the intersection envelope is empty or if the intersection // envelope is (almost) the same of the original envelope we just // return (with different return values). // // // if (cropEnvelope.isEmpty()) throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP)); if (cropEnvelope.equals(sourceEnvelope, scaleFactor / 2.0, false)) return sourceCoverage; // // // // 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) 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) { // ///////////////////////////////////////////////////////////////////// // // 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. // // ///////////////////////////////////////////////////////////////////// // // // // Convert the crop envelope into a polygon and the use the // world-to-grid transform to get a ROI for the source coverage. // // // final Rectangle2D rect = cropEnvelope.toRectangle2D(); final Coordinate[] coord = new Coordinate[] { new Coordinate(rect.getMinX(), rect.getMinY()), new Coordinate(rect.getMinX(), rect.getMaxY()), new Coordinate(rect.getMaxX(), rect.getMaxY()), new Coordinate(rect.getMaxX(), rect.getMinY()), new Coordinate(rect.getMinX(), rect.getMinY()) }; final LinearRing ring = gf.createLinearRing(coord); final Polygon modelSpaceROI = new Polygon(ring, null, gf); // check that we have the same thing here assert modelSpaceROI.getEnvelopeInternal().equals(new ReferencedEnvelope(rect, cropEnvelope.getCoordinateReferenceSystem())); // // // // 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 = decideJAIOperation(parameters,rasterSpaceROI.getBounds2D(), points); if (doMosaic) { assert isSimpleTransform==false; // prepare the params for the mosaic final ROIShape roi = new ROIShape(rasterSpaceROI); pbj.add(MosaicDescriptor.MOSAIC_TYPE_OVERLAY); pbj.add(null); pbj.add(new ROI[] { roi }); pbj.add(null); pbj.add(CoverageUtilities.getBackgroundValues(sourceCoverage)); //prepare the fina layout final Rectangle bounds = roi.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 (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 = "Crop"; } // ///////////////////////////////////////////////////////////////////// // // Apply operation // // ///////////////////////////////////////////////////////////////////// if (!useProvidedProcessor) croppedImage = JAI.create(operatioName, pbj, targetHints); else croppedImage = processor.createNS(operatioName, pbj,targetHints); //conserve the input grid to world transformation return new CroppedCoverage2D( sourceCoverage.getName(), croppedImage, new GridGeometry2D( new GridEnvelope2D(croppedImage.getBounds()), sourceGridGeometry.getGridToCRS2D(PixelOrientation.CENTER), sourceCoverage.getCoordinateReferenceSystem()), sourceCoverage, actionTaken, rasterSpaceROI, hints); } catch (TransformException e) { throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), e); } catch (NoninvertibleTransformException e) { throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), e); } } /** * @param hints * @param sourceImage * @return */ private static RenderingHints prepareHints(Hints hints, final RenderedImage sourceImage) { RenderingHints targetHints = ImageUtilities .getRenderingHints(sourceImage); if (targetHints == null) { targetHints = new RenderingHints(null); } if (hints != null) { targetHints.add(hints); } return targetHints; } /** * Initialize a layout object using the provided {@link RenderedImage} and the provided {@link Hints}. * * @param sourceImage {@link RenderedImage} to use for initializing the returned layout. * @param hints {@link Hints} to use for initializing the returned layout. * @return an {@link ImageLayout} instance. */ private static ImageLayout initLayout(final RenderedImage sourceImage, RenderingHints hints) { ImageLayout layout = (ImageLayout) hints.get(JAI.KEY_IMAGE_LAYOUT); if (layout != null) { layout = (ImageLayout) layout.clone(); } else { layout = new ImageLayout(sourceImage); layout.unsetTileLayout(); // At this point, only the color model and sample model are left // valid. } // crop will ignore minx, miny width and height if ((layout.getValidMask() & (ImageLayout.TILE_WIDTH_MASK | ImageLayout.TILE_HEIGHT_MASK | ImageLayout.TILE_GRID_X_OFFSET_MASK | ImageLayout.TILE_GRID_Y_OFFSET_MASK)) == 0) { layout.setTileGridXOffset(layout.getMinX(sourceImage)); layout.setTileGridYOffset(layout.getMinY(sourceImage)); final int width = layout.getWidth(sourceImage); final int height = layout.getHeight(sourceImage); if (layout.getTileWidth(sourceImage) > width) layout.setTileWidth(width); if (layout.getTileHeight(sourceImage) > height) layout.setTileHeight(height); } return layout; } /** * Decides whether we would benefit from using a mosaic instead of a crop * @param parameters * @param finalGridRange * @param points * @return * @throws InvalidParameterTypeException * @throws ParameterNotFoundException */ private static boolean decideJAIOperation( final ParameterValueGroup parameters, Rectangle2D finalGridRange, final List<Point2D> points) throws InvalidParameterTypeException, ParameterNotFoundException { final double cropArea = finalGridRange.getWidth() * finalGridRange.getHeight(); final double roiArea = Math.abs(Crop.area((Point2D[]) points .toArray(new Point2D[] {}))); final double roiOpt = parameters.parameter("ROITolerance") .doubleValue(); final boolean doMosaic=roiOpt * cropArea > roiArea; return doMosaic; } }