* - User provided only a "grid to CRS" transform. Then, transform the projected
* envelope to "grid units" using the specified transform and create a grid range
* big enough to hold the result.
*/
if (targetGG == null) {
final GridEnvelope targetGR;
targetGR = force2D ? new GridEnvelope2D(sourceGG.getGridRange2D()) : sourceGG.getGridRange();
targetGG = new GridGeometry2D(targetGR, targetEnvelope);
step1 = targetGG.getGridToCRS(CORNER);
} else if (!targetGG.isDefined(GridGeometry2D.GRID_TO_CRS_BITMASK)) {
targetGG = new GridGeometry2D(targetGG.getGridRange(), targetEnvelope);
step1 = targetGG.getGridToCRS(CORNER);
} else {
step1 = targetGG.getGridToCRS(CORNER);
if (!targetGG.isDefined(GridGeometry2D.GRID_RANGE_BITMASK)) {
GeneralEnvelope gridRange = CRS.transform(step1.inverse(), targetEnvelope);
// According OpenGIS specification, GridGeometry maps pixel's center.
targetGG = new GridGeometry2D(new GeneralGridEnvelope(gridRange, PixelInCell.CELL_CENTER), step1, targetCRS);
}
}
/*
* Computes the final transform.
*/
allSteps = mtFactory.createConcatenatedTransform(
mtFactory.createConcatenatedTransform(step1, step2), step3);
}
allSteps2D = toMathTransform2D(allSteps, mtFactory, targetGG);
if (!(allSteps2D instanceof MathTransform2D)) {
// Should not happen with Geotools implementations. May happen
// with some external implementations, but should stay unusual.
throw new TransformException(Errors.format(ErrorKeys.NO_TRANSFORM2D_AVAILABLE));
}
////////////////////////////////////////////////////////////////////////////////////////
//// ////
//// STEP 1: Extracts needed informations from the parameters ////
//// STEP 2: Creates the "target to source" MathTransform ////
//// =======>> STEP 3: Computes the target image layout <<====== ////
//// STEP 4: Applies the JAI operation ("Affine", "Warp", etc) ////
//// ////
////////////////////////////////////////////////////////////////////////////////////////
final RenderingHints targetHints = processingView.getRenderingHints(sourceImage);
if (hints != null) {
targetHints.add(hints);
}
ImageLayout layout = (ImageLayout) targetHints.get(JAI.KEY_IMAGE_LAYOUT);
if (layout != null) {
layout = (ImageLayout) layout.clone();
} else {
layout = new ImageLayout();
// Do not inherit the color model and sample model from the 'sourceImage';
// Let the operation decide itself. This is necessary in case we change the
// source, as we do if we choose the "Mosaic" operation.
}
final Rectangle sourceBB = sourceGG.getGridRange2D();
final Rectangle targetBB = targetGG.getGridRange2D();
if (isBoundsUndefined(layout, false)) {
layout.setMinX (targetBB.x);
layout.setMinY (targetBB.y);
layout.setWidth (targetBB.width);
layout.setHeight(targetBB.height);
}
if (isBoundsUndefined(layout, true)) {
Dimension size = new Dimension(layout.getWidth (sourceImage),
layout.getHeight(sourceImage));
size = ImageUtilities.toTileSize(size);
layout.setTileGridXOffset(layout.getMinX(sourceImage));
layout.setTileGridYOffset(layout.getMinY(sourceImage));
layout.setTileWidth (size.width);
layout.setTileHeight(size.height);
}
/*
* Creates the background values array.
*/
final double[] background = backgroundValues != null ? backgroundValues : CoverageUtilities.getBackgroundValues(sourceCoverage);
/*
* We need to correctly manage the Hints to control the replacement of IndexColorModel.
* It is worth to point out that setting the JAI.KEY_REPLACE_INDEX_COLOR_MODEL hint to
* Boolean.TRUE is not enough to force the operators to do an expansion. If we explicitly
* provide an ImageLayout built with the source image where the CM and the SM are valid.
* those will be employed overriding a the possibility to expand the color model.
*/
if (ViewType.PHOTOGRAPHIC.equals(processingView)) {
layout.unsetValid(ImageLayout.COLOR_MODEL_MASK | ImageLayout.SAMPLE_MODEL_MASK);
}
targetHints.put(JAI.KEY_IMAGE_LAYOUT, layout);
////////////////////////////////////////////////////////////////////////////////////////
//// ////
//// STEP 1: Extracts needed informations from the parameters ////
//// STEP 2: Creates the "target to source" MathTransform ////
//// STEP 3: Computes the target image layout ////
//// =======>> STEP 4: Applies the JAI operation ("Affine", "Warp", etc) <<====== ////
//// ////
////////////////////////////////////////////////////////////////////////////////////////
/*
* If the user requests a new grid geometry with the same coordinate reference system,
* and if the grid geometry is equivalents to a simple extraction of a sub-area, then
* delegates the work to a "Crop" operation.
*/
final String operation;
final ParameterBlock paramBlk = new ParameterBlock().addSource(sourceImage);
final Map<String, Object> imageProperties = new HashMap<String, Object>();
Warp warp = null;
if (allSteps.isIdentity() || (allSteps instanceof AffineTransform &&
XAffineTransform.isIdentity((AffineTransform) allSteps, EPS)))
{
/*
* Since there is no interpolation to perform, use the native view (which may be
* packed or geophysics - it is just the view which is closest to original data).
*/
sourceCoverage = sourceCoverage.view(ViewType.NATIVE);
sourceImage = PlanarImage.wrapRenderedImage(sourceCoverage.getRenderedImage());
paramBlk.removeSources();
paramBlk.addSource(sourceImage);
if (targetBB.equals(sourceBB)) {
/*
* Optimization in case we have nothing to do, not even a crop. Reverts to the
* original coverage BEFORE to creates Resampler2D. Note that while there is
* nothing to do, the target CRS is not identical to the source CRS (so we need
* to create a new coverage) otherwise this condition would have been detected
* sooner in this method.
*/
sourceCoverage = sourceCoverage.view(finalView);
sourceImage = PlanarImage.wrapRenderedImage(sourceCoverage.getRenderedImage());
return create(sourceCoverage, sourceImage, targetGG, ViewType.SAME, null, null, hints);
}
if (sourceBB.contains(targetBB)) {
operation = "Crop";
paramBlk.add(Float.valueOf(targetBB.x))
.add(Float.valueOf(targetBB.y))
.add(Float.valueOf(targetBB.width))
.add(Float.valueOf(targetBB.height));
} else {
operation = "Mosaic";
paramBlk.add(MosaicDescriptor.MOSAIC_TYPE_OVERLAY)
.add(null).add(null).add(null).add(background);
}
} else {
/*
* Special case for the affine transform. Try to use the JAI "Affine" operation
* instead of the more general "Warp" one. JAI provides native acceleration for
* the affine operation.
*
* NOTE 1: There is no need to check for "Scale" and "Translate" as special cases
* of "Affine" since JAI already does this check for us.
*
* NOTE 2: "Affine", "Scale", "Translate", "Rotate" and similar operations ignore
* the 'xmin', 'ymin', 'width' and 'height' image layout. Consequently, we
* can't use this operation if the user provided explicitly a grid range.
*
* NOTE 3: If the user didn't specified any grid geometry, then a yet cheaper approach
* is to just update the 'gridToCRS' value. We returns a grid coverage wrapping
* the SOURCE image with the updated grid geometry.
*/
if ((automaticGR || targetBB.equals(sourceBB)) && allSteps instanceof AffineTransform) {
if (automaticGG) {
// Cheapest approach: just update 'gridToCRS'.
MathTransform mtr;
mtr = sourceGG.getGridToCRS(CORNER);
mtr = mtFactory.createConcatenatedTransform(mtr, step2.inverse());
targetGG = new GridGeometry2D(sourceGG.getGridRange(), mtr, targetCRS);
/*
* Note: do NOT use the "GridGeometry2D(sourceGridRange, targetEnvelope)"
* constructor in the above line. We must give a MathTransform argument to
* the constructor, not an Envelope, because the later infer a MathTransform
* using heuristic rules. Only the constructor with a MathTransform argument
* is fully accurate.
*/
return create(sourceCoverage, sourceImage, targetGG, finalView, null, null, hints);
}
// More general approach: apply the affine transform.
operation = "Affine";
final AffineTransform affine = (AffineTransform) allSteps.inverse();
paramBlk.add(affine).add(interpolation).add(background);
} else {
/*
* General case: constructs the warp transform.
*
* TODO: JAI 1.1.3 seems to have a bug when the target envelope is greater than
* the source envelope: Warp on float values doesn't set to 'background'
* the points outside the envelope. The operation seems to work correctly
* on integer values, so as a workaround we restart the operation without
* interpolation (which increase the chances to get it down on integers).
* Remove this hack when this JAI bug will be fixed.
*
* TODO: Move the check for AffineTransform into WarpTransform2D.
*/
boolean forceAdapter = false;
switch (sourceImage.getSampleModel().getTransferType()) {
case DataBuffer.TYPE_DOUBLE:
case DataBuffer.TYPE_FLOAT: {
Envelope source = CRS.transform(sourceGG.getEnvelope(), targetCRS);
Envelope target = CRS.transform(targetGG.getEnvelope(), targetCRS);
source = targetGG.reduce(source);
target = targetGG.reduce(target);
if (!(new GeneralEnvelope(source).contains(target, true))) {
if (interpolation != null && !(interpolation instanceof InterpolationNearest)) {
return reproject(sourceCoverage, targetCRS, targetGG, null, hints, background);
} else {
// If we were already using nearest-neighbor interpolation, force
// usage of WarpAdapter2D instead of WarpAffine. The price will be
// a slower reprojection.
forceAdapter = true;
}
}
}
}
// -------- End of JAI bug workaround --------
final MathTransform2D transform = (MathTransform2D) allSteps2D;
final CharSequence name = sourceCoverage.getName();
operation = "Warp";
if (forceAdapter) {
warp = new WarpBuilder(0.0).buildWarp(transform, sourceBB);
} else {
warp = createWarp(name, sourceBB, targetBB, transform, mtFactory, hints);
}
// store the transormation in the properties, as we might want to retrieve and chain
// it with affine transforms down the chain
imageProperties.put("MathTransform", transform);
imageProperties.put("SourceBoundingBox", sourceBB);
paramBlk.add(warp).add(interpolation).add(background);
}
}
final RenderedOp targetImage = getJAI(hints).createNS(operation, paramBlk, targetHints);
for (Map.Entry<String, Object> entry : imageProperties.entrySet()) {
targetImage.setProperty(entry.getKey(), entry.getValue());
}
final Locale locale = sourceCoverage.getLocale(); // For logging purpose.
/*
* The JAI operation sometime returns an image with a bounding box different than what we
* expected. This is true especially for the "Affine" operation: the JAI documentation said
* explicitly that xmin, ymin, width and height image layout hints are ignored for this one.
* As a safety, we check the bounding box in any case. If it doesn't matches, then we will
* reconstruct the target grid geometry.
*/
final GridEnvelope targetGR = targetGG.getGridRange();
final int[] lower = targetGR.getLow().getCoordinateValues();
final int[] upper = targetGR.getHigh().getCoordinateValues();
for (int i=0; i<upper.length; i++) {
upper[i]++; // Make them exclusive.
}
lower[targetGG.gridDimensionX] = targetImage.getMinX();
lower[targetGG.gridDimensionY] = targetImage.getMinY();
upper[targetGG.gridDimensionX] = targetImage.getMaxX();
upper[targetGG.gridDimensionY] = targetImage.getMaxY();
final GridEnvelope actualGR = new GeneralGridEnvelope(lower, upper);
if (!targetGR.equals(actualGR)) {
targetGG = new GridGeometry2D(actualGR, targetGG.getGridToCRS(PixelInCell.CELL_CENTER),targetCRS);
if (!automaticGR) {
log(Loggings.getResources(locale).getLogRecord(Level.FINE,
LoggingKeys.ADJUSTED_GRID_GEOMETRY_$1, sourceCoverage.getName().toString(locale)));