/*
* 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.Shape;
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.GridCoverageFactory;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.processing.CannotCropException;
import org.geotools.coverage.processing.Operation2D;
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.JTS;
import org.geotools.geometry.jts.LiteShape2;
import org.geotools.image.crop.GTCropDescriptor;
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.coverage.CoverageUtilities;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.resources.coverage.IntersectUtils;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.opengis.coverage.Coverage;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.parameter.InvalidParameterTypeException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.TransformException;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.PrecisionModel;
/**
* 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>ROI</strong><br>
* By providing a ROI parameter, the coverage can be cropped by any set of
* polygons, even disjuncted ones. When the ROI is provided, the JAI's Mosaic
* operation will be used.
* <br>At least one between <i>Envelope</i> and <i>ROI</i> must be provided.
* If both of them are provided, the resulting area will be the intersection
* of them.
* <br>ROI geometries must be in the same CRS as the source coverage.
* <br>ROI must be any among a {@link com.vividsolutions.jts.geom.Polygon}, a
* {@link MultiPolygon}, or a {@link GeometryCollection} of the two.
*
* <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 more t,z friendly
* @version $Id$
* @author Simone Giannecchini (GeoSolutions)
* @author Emanuele Tajariol (GeoSolutions)
* @since 2.3
*
* @see javax.media.jai.operator.CropDescriptor
*/
public class Crop extends Operation2D {
/**
* Serial number for cross-version compatibility.
*/
private static final long serialVersionUID = 4466072819239413456L;
public static final double EPS = 1E-3;
private final static GeometryFactory GFACTORY;
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);
final PrecisionModel pm;
if (o != null)
pm = (PrecisionModel) o;
else {
pm = new PrecisionModel();
}
GFACTORY = new GeometryFactory(pm, 0);
// Register manually the GTCrop operation, in web containers JAI registration may fails
GTCropDescriptor.register();
}
public static final String PARAMNAME_ENVELOPE = "Envelope";
public static final String PARAMNAME_ROI = "ROI";
public static final String PARAMNAME_ROITOLERANCE = "ROITolerance";
/**
* 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, PARAMNAME_ENVELOPE,
Envelope.class, // Value class
null, // Array of valid values
null, // Default value
null, // Minimal value
null, // Maximal value
null, // Unit of measure
true); // Parameter is optional
/**
* The parameter descriptor used to pass this operation the polygons(s) to use
* when doing the spatial crop.
*
* If set, the intersection of Envelope and ROI must not be empty.
* The final output area will contain area inside the Envelope AND the ROI.
*
* The parameter shall be a Polygon instance, or a GeometryCollection holding Polygons
*/
public static final ParameterDescriptor<Geometry> CROP_ROI = new DefaultParameterDescriptor<Geometry>(
Citations.JAI, PARAMNAME_ROI,
Geometry.class, // Value class
null, // Array of valid values
null, // Default value
null, // Minimal value
null, // Maximal value
null, // Unit of measure
true); // 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, PARAMNAME_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
/**
* Constructs a default {@code "Crop"} operation.
*/
public Crop() {
super(new DefaultParameterDescriptorGroup(Citations.GEOTOOLS,
"CoverageCrop", new ParameterDescriptor[] { SOURCE_0,
CROP_ENVELOPE, CROP_ROI,
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)
*/
@SuppressWarnings("unchecked")
public Coverage doOperation(ParameterValueGroup parameters, Hints hints) {
final Geometry cropRoi; // extracted from parameters
GeneralEnvelope cropEnvelope = null; // extracted from parameters
final GridCoverage2D source; // extracted from parameters
final double roiTolerance = parameters.parameter(Crop.PARAMNAME_ROITOLERANCE).doubleValue();
// /////////////////////////////////////////////////////////////////////
//
// Assigning and 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()));
}
source = (GridCoverage2D) sourceParameter.getValue();
// Check Envelope and ROI existence - we need at least one of them
final ParameterValue envelopeParameter = parameters.parameter(PARAMNAME_ENVELOPE);
final ParameterValue roiParameter = parameters.parameter(PARAMNAME_ROI);
if ( (envelopeParameter == null || envelopeParameter.getValue() == null) &&
(roiParameter == null || roiParameter.getValue() ==null) )
throw new CannotCropException(Errors.format(ErrorKeys.NULL_PARAMETER_$2, PARAMNAME_ENVELOPE, GeneralEnvelope.class.toString()));
Object envelope = envelopeParameter.getValue();
if (envelope != null){
if (envelope instanceof GeneralEnvelope){
cropEnvelope = (GeneralEnvelope) envelope;
} else if (envelope instanceof Envelope){
cropEnvelope = new GeneralEnvelope((Envelope)envelope);
}
}
// may be null
// Check crop ROI
try {
cropRoi = IntersectUtils.unrollGeometries((Geometry) roiParameter.getValue()); // may throw if format not correct
} catch (IllegalArgumentException ex) {
throw new CannotCropException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, PARAMNAME_ROI, ex.getMessage()), ex);
}
// Setting a GeneralEnvelope from ROI if needed
if (cropRoi != null && cropEnvelope == null) {
Envelope e2d = JTS.getEnvelope2D(cropRoi.getEnvelopeInternal(), source.getCoordinateReferenceSystem());
cropEnvelope = new GeneralEnvelope(e2d);
}
// /////////////////////////////////////////////////////////////////////
//
// Initialization
//
// We take the crop envelope and the source envelope then we check their
// crs and we also check if they ever overlap.
//
// /////////////////////////////////////////////////////////////////////
// envelope of the source coverage
final Envelope2D sourceEnvelope = source.getEnvelope2D();
// crop envelope
Envelope2D destinationEnvelope = new Envelope2D(cropEnvelope);
CoordinateReferenceSystem sourceCRS = sourceEnvelope.getCoordinateReferenceSystem();
CoordinateReferenceSystem 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 be equals
//
// //
if (!CRS.equalsIgnoreMetadata(sourceCRS, destinationCRS)) {
throw new CannotCropException(Errors.format(ErrorKeys.MISMATCHED_ENVELOPE_CRS_$2, sourceCRS.getName().getCode(), destinationCRS.getName().getCode()));
}
if(cropRoi != null) {
// TODO: check ROI SRID
}
// //
//
// 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));
// intersect the ROI with the intersection envelope and throw an error if they do not intersect
if(cropRoi != null) {
final Geometry jis = JTS.shapeToGeometry(intersectionEnvelope.toRectangle2D(), cropRoi.getFactory());
if( ! IntersectUtils.intersects(cropRoi, jis))
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 sourceCornerGridToWorld = (AffineTransform) ((GridGeometry2D) source.getGridGeometry()).getGridToCRS(PixelInCell.CELL_CORNER);
// //
//
// 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(sourceCornerGridToWorld);
if (cropRoi != null || !intersectionEnvelope.equals(sourceEnvelope, tolerance / 2.0, false)) {
cropEnvelope = intersectionEnvelope.clone();
return buildResult(
cropEnvelope,
cropRoi,
roiTolerance,
(hints instanceof Hints) ? (Hints) hints: new Hints(hints),
source,
sourceCornerGridToWorld);
} 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;
}
}
/**
* 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 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 = decideJAIOperation(roiTolerance, rasterSpaceROI.getBounds2D(), points);
if (doMosaic || cropROI != null) {
// prepare the params for the mosaic
final ROI[] roiarr;
try {
if(cropROI != null) {
final Shape cropRoiLS2 = new LiteShape2(cropROI, ProjectiveTransform.create(sourceWorldToGridTransform), null, false);
final ROIShape cropRS = new ROIShape(cropRoiLS2);
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 (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
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 },
rasterSpaceROI != null ?Collections.singletonMap("GC_ROI", rasterSpaceROI) :null
);
} catch (TransformException e) {
throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), e);
} catch (NoninvertibleTransformException e) {
throw new CannotCropException(Errors.format(ErrorKeys.CANT_CROP), e);
}
}
/**
* 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,
final 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 double roiTolerance,
final Rectangle2D finalGridRange,
final List<Point2D> points) throws InvalidParameterTypeException,
ParameterNotFoundException {
final double cropArea = finalGridRange.getWidth()* finalGridRange.getHeight();
final double roiArea = Math.abs(FeatureUtilities.area((Point2D[]) points.toArray(new Point2D[] {})));
final boolean doMosaic = roiTolerance * cropArea > roiArea;
return doMosaic;
}
}