/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-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.renderer.lite.gridcoverage2d;
// J2SE dependencies
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImagingOpException;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.media.jai.BorderExtender;
import javax.media.jai.ImageLayout;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.TypeMap;
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.CoverageProcessor;
import org.geotools.coverage.processing.operation.Crop;
import org.geotools.coverage.processing.operation.Resample;
import org.geotools.coverage.processing.operation.Scale;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.ImageWorker;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.image.ImageUtilities;
import org.geotools.styling.RasterSymbolizer;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.filter.expression.Expression;
import org.opengis.metadata.spatial.PixelOrientation;
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.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import com.sun.media.jai.util.Rational;
import com.vividsolutions.jts.geom.Envelope;
/**
* A helper class for rendering {@link GridCoverage} objects.
* @author Simone Giannecchini, GeoSolutions SAS
* @author Andrea Aime, GeoSolutions SAS
* @author Alessio Fabiani, GeoSolutions SAS
*
* @source $URL$
* @version $Id$
*
*/
@SuppressWarnings("deprecation")
public final class GridCoverageRenderer {
/**
* This variable is use for testing purposes in order to force this
* {@link GridCoverageRenderer} to dump images at various steps on the disk.
*/
private static boolean DEBUG = Boolean
.getBoolean("org.geotools.renderer.lite.gridcoverage2d.debug");
private static String debugDir;
static {
if (DEBUG) {
final File tempDir = new File(System.getProperty("user.home"),"gt-renderer");
if (!tempDir.exists() ) {
if(!tempDir.mkdir())
System.out
.println("Unable to create debug dir, exiting application!!!");
DEBUG=false;
debugDir = null;
} else
{
debugDir = tempDir.getAbsolutePath();
System.out.println("Rendering debug dir "+debugDir);
}
}
}
/**
* Simple pair class for holding a {@link GridCoverage2D} with the final WorldToGrid.
*
* @author Simone Giannecchini, GeoSolutions SAS.
*
*/
private final static class GCpair {
private final AffineTransform transform;
private final GridCoverage2D gc;
public GCpair(AffineTransform transform, GridCoverage2D gc) {
this.transform = transform;
this.gc = gc;
}
public AffineTransform getTransform() {
return transform;
}
public GridCoverage2D getGridCoverage() {
return gc;
}
}
/**
* Helper function
* * @param symbolizer
*/
static float getOpacity(RasterSymbolizer symbolizer) {
float alpha = 1.0f;
Expression exp = symbolizer.getOpacity();
if (exp == null){
return alpha;
}
Number number = (Number) exp.evaluate(null,Float.class);
if (number == null){
return alpha;
}
return number.floatValue();
}
/** Cached factory for the {@link Crop} operation. */
private final static Crop coverageCropFactory = new Crop();
/** Logger. */
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger("org.geotools.rendering");
static {
// ///////////////////////////////////////////////////////////////////
//
// Caching parameters for performing the various operations.
//
// ///////////////////////////////////////////////////////////////////
final CoverageProcessor processor = new CoverageProcessor(new Hints(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE));
resampleParams = processor.getOperation("Resample").getParameters();
cropParams = processor.getOperation("CoverageCrop").getParameters();
}
/** The Display (User defined) CRS * */
private final CoordinateReferenceSystem destinationCRS;
/** Area we want to draw. */
private final GeneralEnvelope destinationEnvelope;
/** Size of the area we want to draw in pixels. */
private final Rectangle destinationSize;
private final AffineTransform finalGridToWorld;
private final AffineTransform finalWorldToGrid;
private final Hints hints = new Hints();
// FORMULAE FOR FORWARD MAP are derived as follows
// Nearest
// Minimum:
// srcMin = floor ((dstMin + 0.5 - trans) / scale)
// srcMin <= (dstMin + 0.5 - trans) / scale < srcMin + 1
// srcMin*scale <= dstMin + 0.5 - trans < (srcMin + 1)*scale
// srcMin*scale - 0.5 + trans
// <= dstMin < (srcMin + 1)*scale - 0.5 + trans
// Let A = srcMin*scale - 0.5 + trans,
// Let B = (srcMin + 1)*scale - 0.5 + trans
//
// dstMin = ceil(A)
//
// Maximum:
// Note that srcMax is defined to be srcMin + dimension - 1
// srcMax = floor ((dstMax + 0.5 - trans) / scale)
// srcMax <= (dstMax + 0.5 - trans) / scale < srcMax + 1
// srcMax*scale <= dstMax + 0.5 - trans < (srcMax + 1)*scale
// srcMax*scale - 0.5 + trans
// <= dstMax < (srcMax+1) * scale - 0.5 + trans
// Let float A = (srcMax + 1) * scale - 0.5 + trans
//
// dstMax = floor(A), if floor(A) < A, else
// dstMax = floor(A) - 1
// OR dstMax = ceil(A - 1)
//
// Other interpolations
//
// First the source should be shrunk by the padding that is
// required for the particular interpolation. Then the
// shrunk source should be forward mapped as follows:
//
// Minimum:
// srcMin = floor (((dstMin + 0.5 - trans)/scale) - 0.5)
// srcMin <= ((dstMin + 0.5 - trans)/scale) - 0.5 < srcMin+1
// (srcMin+0.5)*scale <= dstMin+0.5-trans <
// (srcMin+1.5)*scale
// (srcMin+0.5)*scale - 0.5 + trans
// <= dstMin < (srcMin+1.5)*scale - 0.5 + trans
// Let A = (srcMin+0.5)*scale - 0.5 + trans,
// Let B = (srcMin+1.5)*scale - 0.5 + trans
//
// dstMin = ceil(A)
//
// Maximum:
// srcMax is defined as srcMin + dimension - 1
// srcMax = floor (((dstMax + 0.5 - trans) / scale) - 0.5)
// srcMax <= ((dstMax + 0.5 - trans)/scale) - 0.5 < srcMax+1
// (srcMax+0.5)*scale <= dstMax + 0.5 - trans <
// (srcMax+1.5)*scale
// (srcMax+0.5)*scale - 0.5 + trans
// <= dstMax < (srcMax+1.5)*scale - 0.5 + trans
// Let float A = (srcMax+1.5)*scale - 0.5 + trans
//
// dstMax = floor(A), if floor(A) < A, else
// dstMax = floor(A) - 1
// OR dstMax = ceil(A - 1)
//
private static float rationalTolerance = 0.000001F;
/** Parameters used to control the {@link Resample} operation. */
private final static ParameterValueGroup resampleParams;
/** Parameters used to control the {@link Crop} operation. */
private static ParameterValueGroup cropParams;
/** Parameters used to control the {@link Scale} operation. */
private static final Resample resampleFactory = new Resample();
/**
* Creates a new {@link GridCoverageRenderer} object.
*
* @param destinationCRS
* the CRS of the {@link GridCoverage2D} to render.
* @param envelope
* delineating the area to be rendered.
* @param screenSize
* at which we want to rendere the source
* {@link GridCoverage2D}.
* @param worldToScreen if not <code>null</code> and if it contains a rotation,
* this Affine Tranform is used directly to convert from world coordinates
* to screen coordinates. Otherwise, a standard {@link GridToEnvelopeMapper}
* is used to calculate the affine transform.
*
* @throws TransformException
* @throws NoninvertibleTransformException
*/
public GridCoverageRenderer(final CoordinateReferenceSystem destinationCRS,
final Envelope envelope, Rectangle screenSize, AffineTransform worldToScreen)
throws TransformException, NoninvertibleTransformException {
this(destinationCRS, envelope, screenSize, worldToScreen, null);
}
/**
* Creates a new {@link GridCoverageRenderer} object.
*
* @param destinationCRS
* the CRS of the {@link GridCoverage2D} to render.
* @param envelope
* delineating the area to be rendered.
* @param screenSize
* at which we want to rendere the source
* {@link GridCoverage2D}.
* @param worldToScreen if not <code>null</code> and if it contains a rotation,
* this Affine Tranform is used directly to convert from world coordinates
* to screen coordinates. Otherwise, a standard {@link GridToEnvelopeMapper}
* is used to calculate the affine transform.
*
* @param hints
* {@link RenderingHints} to control this rendering process.
* @throws TransformException
* @throws NoninvertibleTransformException
*/
public GridCoverageRenderer(
final CoordinateReferenceSystem destinationCRS,
final Envelope envelope,
final Rectangle screenSize,
final AffineTransform worldToScreen,
final RenderingHints hints) throws TransformException,
NoninvertibleTransformException {
// ///////////////////////////////////////////////////////////////////
//
// Initialize this renderer
//
// ///////////////////////////////////////////////////////////////////
this.destinationSize = screenSize;
this.destinationCRS = CRS.getHorizontalCRS(destinationCRS);
if (this.destinationCRS == null)
throw new TransformException(Errors.format(
ErrorKeys.CANT_SEPARATE_CRS_$1, destinationCRS));
destinationEnvelope = new GeneralEnvelope(new ReferencedEnvelope(envelope, destinationCRS));
// ///////////////////////////////////////////////////////////////////
//
// FINAL DRAWING DIMENSIONS AND RESOLUTION
// I am here getting the final drawing dimensions (on the device) and
// the resolution for this rendererbut in the CRS of the source coverage
// since I am going to compare this info with the same info for the
// source coverage.
//
// ///////////////////////////////////////////////////////////////////
// PHUSTAD : The gridToEnvelopeMapper does not handle rotated views.
//
if (worldToScreen != null && XAffineTransform.getRotation(worldToScreen) != 0.0) {
finalWorldToGrid = new AffineTransform(worldToScreen);
finalGridToWorld = finalWorldToGrid.createInverse();
} else {
final GridToEnvelopeMapper gridToEnvelopeMapper = new GridToEnvelopeMapper();
gridToEnvelopeMapper.setPixelAnchor(PixelInCell.CELL_CORNER);
gridToEnvelopeMapper.setGridRange(new GridEnvelope2D(destinationSize));
gridToEnvelopeMapper.setEnvelope(destinationEnvelope);
finalGridToWorld = new AffineTransform(gridToEnvelopeMapper.createAffineTransform());
finalWorldToGrid = finalGridToWorld.createInverse();
}
//
// HINTS
//
if (hints != null)
this.hints.add(hints);
// this prevents users from overriding lenient hint
this.hints.put(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE);
this.hints.add(ImageUtilities.DONT_REPLACE_INDEX_COLOR_MODEL);
}
/**
* Reprojecting the input coverage using the provided parameters.
*
* @param gc
* @param crs
* @param interpolation
* @return
* @throws FactoryException
*/
private static GridCoverage2D resample(final GridCoverage2D gc,
CoordinateReferenceSystem crs, final Interpolation interpolation,
final GeneralEnvelope destinationEnvelope, final Hints hints) throws FactoryException {
// paranoiac check
assert CRS.equalsIgnoreMetadata(destinationEnvelope
.getCoordinateReferenceSystem(), crs)
|| CRS
.findMathTransform(
destinationEnvelope
.getCoordinateReferenceSystem(), crs)
.isIdentity();
final ParameterValueGroup param = (ParameterValueGroup) resampleParams
.clone();
param.parameter("source").setValue(gc);
param.parameter("CoordinateReferenceSystem").setValue(crs);
param.parameter("InterpolationType").setValue(interpolation);
return (GridCoverage2D) resampleFactory.doOperation(param, hints);
}
/**
* Cropping the provided coverage to the requested geographic area.
*
* @param gc
* @param envelope
* @param crs
* @return
*/
private static GridCoverage2D getCroppedCoverage(GridCoverage2D gc,
GeneralEnvelope envelope, CoordinateReferenceSystem crs, final Hints hints) {
final GeneralEnvelope oldEnvelope = (GeneralEnvelope) gc.getEnvelope();
// intersect the envelopes in order to prepare for crooping the coverage
// down to the neded resolution
final GeneralEnvelope intersectionEnvelope = new GeneralEnvelope(
envelope);
intersectionEnvelope.setCoordinateReferenceSystem(crs);
intersectionEnvelope.intersect((GeneralEnvelope) oldEnvelope);
// Do we have something to show? After the crop I could get a null
// coverage which would mean nothing to show.
if (intersectionEnvelope.isEmpty())
return null;
// crop
final ParameterValueGroup param = (ParameterValueGroup) cropParams
.clone();
param.parameter("source").setValue(gc);
param.parameter("Envelope").setValue(intersectionEnvelope);
return (GridCoverage2D) coverageCropFactory.doOperation(param, hints);
}
/**
* Write the provided {@link RenderedImage} in the debug directory with the provided file name.
*
* @param raster
* the {@link RenderedImage} that we have to write.
* @param fileName
* a {@link String} indicating where we should write it.
*/
static void writeRenderedImage(final RenderedImage raster, final String fileName) {
if (debugDir == null)
throw new NullPointerException(
"Unable to write the provided coverage in the debug directory");
if (DEBUG == false)
throw new IllegalStateException(
"Unable to write the provided coverage since we are not in debug mode");
try {
ImageIO.write(raster, "tiff", new File(debugDir, fileName + ".tiff"));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
}
/**
* Builds a (RenderedImage, AffineTransform) pair that can be used for rendering onto a
* {@link Graphics2D} or as the basis to build a final image. Will return null if there is
* nothing to render.
*
* @param gridCoverage
* @param symbolizer
* @return
* @throws FactoryException
* @throws TransformException
* @throws NoninvertibleTransformException
*/
private GCpair prepareFinalImage(
final GridCoverage2D gridCoverage,
final RasterSymbolizer symbolizer
)throws FactoryException, TransformException,NoninvertibleTransformException {
// Initial checks
if(gridCoverage==null)
throw new NullPointerException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1,"gridCoverage"));
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Drawing coverage "+gridCoverage.toString());
//
// Getting information about the source coverage like the source CRS,
// the source envelope and the source geometry.
//
final CoordinateReferenceSystem sourceCoverageCRS = gridCoverage.getCoordinateReferenceSystem2D();
final GeneralEnvelope sourceCoverageEnvelope = (GeneralEnvelope) gridCoverage.getEnvelope();
//
// GET THE CRS MAPPING
//
// This step I instantiate the MathTransform for going from the source
// crs to the destination crs.
//
// math transform from source to target crs
final MathTransform sourceCRSToDestinationCRSTransformation = CRS.findMathTransform(sourceCoverageCRS, destinationCRS, true);
final MathTransform destinationCRSToSourceCRSTransformation = sourceCRSToDestinationCRSTransformation.inverse();
final boolean doReprojection = !sourceCRSToDestinationCRSTransformation.isIdentity();
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Transforming coverage envelope with transform "+destinationCRSToSourceCRSTransformation.toWKT());
// //
//
// Do we need reprojection?
//
// //
GeneralEnvelope destinationEnvelopeInSourceCRS;
if (doReprojection) {
// /////////////////////////////////////////////////////////////////////
//
// PHASE 1
//
// PREPARING THE REQUESTED ENVELOPE FOR LATER INTERSECTION
//
// /////////////////////////////////////////////////////////////////////
// //
//
// Try to convert the destination envelope in the source crs. If
// this fails we pass through WGS84 as an intermediate step
//
// //
try {
// convert the destination envelope to the source coverage
// native crs in order to try and crop it. If we get an error we
// try to
// do this in two steps using WGS84 as a pivot. This introduces
// some erros (it usually
// increases the envelope we want to check) but it is still
// useful.
destinationEnvelopeInSourceCRS = CRS.transform(
destinationCRSToSourceCRSTransformation,
destinationEnvelope);
} catch (TransformException te) {
// //
//
// Convert the destination envelope to WGS84 if needed for safer
// comparisons later on with the original crs of this coverage.
//
// //
final GeneralEnvelope destinationEnvelopeWGS84;
if (!CRS.equalsIgnoreMetadata(destinationCRS,
DefaultGeographicCRS.WGS84)) {
// get a math transform to go to WGS84
final MathTransform destinationCRSToWGS84transformation = CRS
.findMathTransform(destinationCRS,
DefaultGeographicCRS.WGS84, true);
if (!destinationCRSToWGS84transformation.isIdentity()) {
destinationEnvelopeWGS84 = CRS.transform(
destinationCRSToWGS84transformation,
destinationEnvelope);
destinationEnvelopeWGS84
.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84);
} else {
destinationEnvelopeWGS84 = new GeneralEnvelope(
destinationEnvelope);
}
} else {
destinationEnvelopeWGS84 = new GeneralEnvelope(
destinationEnvelope);
}
// //
//
// Convert the requested envelope from WGS84 to the source crs
// for cropping the provided coverage.
//
// //
if (!CRS.equalsIgnoreMetadata(sourceCoverageCRS,
DefaultGeographicCRS.WGS84)) {
// get a math transform to go to WGS84
final MathTransform WGS84ToSourceCoverageCRSTransformation = CRS
.findMathTransform(DefaultGeographicCRS.WGS84,
sourceCoverageCRS, true);
if (!WGS84ToSourceCoverageCRSTransformation.isIdentity()) {
destinationEnvelopeInSourceCRS = CRS.transform(
WGS84ToSourceCoverageCRSTransformation,
destinationEnvelopeWGS84);
destinationEnvelopeInSourceCRS
.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84);
} else {
destinationEnvelopeInSourceCRS = new GeneralEnvelope(
destinationEnvelopeWGS84);
}
} else {
destinationEnvelopeInSourceCRS = new GeneralEnvelope(
destinationEnvelopeWGS84);
}
}
} else
destinationEnvelopeInSourceCRS = new GeneralEnvelope(destinationEnvelope);
// /////////////////////////////////////////////////////////////////////
//
// NOW CHECKING THE INTERSECTION IN WGS84
//
// //
//
// If the two envelopes intersect each other in WGS84 we are
// reasonably sure that they intersect
//
// /////////////////////////////////////////////////////////////////////
final GeneralEnvelope intersectionEnvelope = new GeneralEnvelope(destinationEnvelopeInSourceCRS);
intersectionEnvelope.intersect(sourceCoverageEnvelope);
if (intersectionEnvelope.isEmpty()||intersectionEnvelope.isNull()) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("The destination envelope does not intersect the envelope of the source coverage.");
}
return null;
}
final Interpolation interpolation = (Interpolation) hints.get(JAI.KEY_INTERPOLATION);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Using interpolation "+interpolation);
// /////////////////////////////////////////////////////////////////////
//
// CROPPING Coverage
//
// /////////////////////////////////////////////////////////////////////
GridCoverage2D preResample=gridCoverage;
try{
preResample = getCroppedCoverage(gridCoverage, intersectionEnvelope, sourceCoverageCRS,this.hints);
if (preResample == null) {
// nothing to render, the AOI does not overlap
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Skipping current coverage because cropped to an empty area");
return null;
}
}catch (Throwable t) {
////
//
// If it happens that the crop fails we try to proceed since the crop does only an optimization. Things might
// work out anyway.
//
////
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,"Crop Failed for reason: "+t.getLocalizedMessage(),t);
preResample=gridCoverage;
}
if (DEBUG) {
writeRenderedImage(preResample.geophysics(false).getRenderedImage(),"preresample");
}
// /////////////////////////////////////////////////////////////////////
//
// REPROJECTION if needed
//
// /////////////////////////////////////////////////////////////////////
GridCoverage2D preSymbolizer;
if (doReprojection) {
preSymbolizer = resample(preResample, destinationCRS,interpolation == null ? new InterpolationNearest(): interpolation, destinationEnvelope,this.hints);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Reprojecting to crs "+ destinationCRS.toWKT());
} else
preSymbolizer = preResample;
if (DEBUG) {
writeRenderedImage(preSymbolizer.geophysics(false).getRenderedImage(),"preSymbolizer");
}
// ///////////////////////////////////////////////////////////////////
//
// RASTERSYMBOLIZER
//
// ///////////////////////////////////////////////////////////////////
final GridCoverage2D symbolizerGC;
final RenderedImage symbolizerImage;
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuilder("Raster Symbolizer ").toString());
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuffer("Raster Symbolizer ").toString());
if(symbolizer!=null){
final RasterSymbolizerHelper rsp = new RasterSymbolizerHelper (preSymbolizer,this.hints);
rsp.visit(symbolizer);
symbolizerGC = (GridCoverage2D) rsp.getOutput();
symbolizerImage = symbolizerGC.geophysics(false).getRenderedImage();
} else {
symbolizerGC = preSymbolizer;
symbolizerImage = symbolizerGC.geophysics(false).getRenderedImage();
}
if (DEBUG) {
writeRenderedImage(symbolizerImage,"postSymbolizer");
}
// ///////////////////////////////////////////////////////////////////
// Apply opacity if needs be
// TODO: move this into the RasterSymbolizerHelper
// ///////////////////////////////////////////////////////////////////
final RenderedImage finalImage;
final GridCoverage2D finalGC;
float opacity = getOpacity(symbolizer);
if(opacity < 1) {
ImageWorker ow = new ImageWorker(symbolizerImage);
finalImage = ow.applyOpacity(opacity).getRenderedImage();
final int numBands=finalImage.getSampleModel().getNumBands();
final GridSampleDimension [] sd= new GridSampleDimension[numBands];
for(int i=0;i<numBands;i++) {
sd[i]= new GridSampleDimension(TypeMap.getColorInterpretation(finalImage.getColorModel(), i).name());
}
GridCoverageFactory factory = CoverageFactoryFinder.getGridCoverageFactory(hints);
finalGC = factory.create(
"opacity_"+symbolizerGC.getName().toString(),
finalImage,
symbolizerGC.getGridGeometry(),
sd,
new GridCoverage[]{symbolizerGC},
symbolizerGC.getProperties()
);
} else {
finalImage = symbolizerImage;
finalGC = symbolizerGC;
}
// ///////////////////////////////////////////////////////////////////
//
// DRAW ME
// I need the grid to world transform for drawing this grid coverage to
// the display
//
// ///////////////////////////////////////////////////////////////////
final GridGeometry2D recoloredCoverageGridGeometry = ((GridGeometry2D) finalGC.getGridGeometry());
// I need to translate half of a pixel since in wms the envelope
// map to the corners of the raster space not to the center of the
// pixels.
final MathTransform2D finalGCTransform=recoloredCoverageGridGeometry.getGridToCRS2D(PixelOrientation.UPPER_LEFT);
if (!(finalGCTransform instanceof AffineTransform)) {
throw new UnsupportedOperationException(
"Non-affine transformations not yet implemented"); // TODO
}
final AffineTransform finalGCgridToWorld = new AffineTransform((AffineTransform) finalGCTransform);
// //
//
// I am going to concatenate the final world to grid transform for the
// screen area with the grid to world transform of the input coverage.
//
// This way i right away position the coverage at the right place in the
// area of interest for the device.
//
// //
final AffineTransform clonedFinalWorldToGrid = (AffineTransform) finalWorldToGrid.clone();
clonedFinalWorldToGrid.concatenate(finalGCgridToWorld);
return new GCpair(clonedFinalWorldToGrid,finalGC);
}
/**
* Turns the coverage into a rendered image applying the necessary transformations and the
* symbolizer
*
* @param gridCoverage
* @param symbolizer
* @return The transformed image, or null if the coverage does not lie within the rendering
* bounds
* @throws FactoryException
* @throws TransformException
* @throws NoninvertibleTransformException
*/
public RenderedImage renderImage(
final GridCoverage2D gridCoverage,
final RasterSymbolizer symbolizer,
final Interpolation interpolation,
final Color background,
final int tileSizeX,
final int tileSizeY
) throws FactoryException, TransformException, NoninvertibleTransformException {
// Build the final image and the associated world to grid transformation
final GCpair couple = prepareFinalImage(gridCoverage, symbolizer);
if (couple == null)
return null;
// NOTICE that at this stage the image we get should be 8 bits, either RGB, RGBA, Gray, GrayA
// either multiband or indexed. It could also be 16 bits indexed!!!!
final RenderedImage finalImage = couple.getGridCoverage().getRenderedImage();
final AffineTransform finalRaster2Model = couple.getTransform();
//paranoiac check to avoid that JAI freaks out when computing its internal layouT on images that are too small
Rectangle2D finalLayout= layoutHelper(
finalImage,
(float)finalRaster2Model.getScaleX(),
(float)finalRaster2Model.getScaleY(),
(float)finalRaster2Model.getTranslateX(),
(float)finalRaster2Model.getTranslateY(),
interpolation);
if(finalLayout.isEmpty()){
if(LOGGER.isLoggable(java.util.logging.Level.FINE))
LOGGER.fine("Unable to create a granuleDescriptor "+this.toString()+ " due to jai scale bug");
return null;
}
// final transformation
final ImageLayout layout = new ImageLayout(finalImage);
layout.setTileGridXOffset(0).setTileGridYOffset(0).setTileHeight(tileSizeY).setTileWidth(tileSizeX);
final RenderingHints localHints = this.hints.clone();
localHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
//add hints to preserve IndexColorModel
if(interpolation instanceof InterpolationNearest)
localHints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE));
//SG add hints for the border extender
localHints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER,BorderExtender.createInstance(BorderExtender.BORDER_COPY)));
RenderedImage im=null;
try {
ImageWorker iw = new ImageWorker(finalImage);
iw.setRenderingHints(localHints);
iw.affine(finalRaster2Model, interpolation, null);
im = iw.getRenderedImage();
} finally {
if(DEBUG)
writeRenderedImage(im, "postAffine");
}
return im;
}
/**
* Paint this grid coverage. The caller must ensure that
* <code>graphics</code> has an affine transform mapping "real world"
* coordinates in the coordinate system given by {@link
* #getCoordinateSystem}.
*
* @param graphics
* the {@link Graphics2D} context in which to paint.
* @param metaBufferedEnvelope
* @throws FactoryException
* @throws TransformException
* @throws NoninvertibleTransformException
* @throws Exception
* @throws UnsupportedOperationException
* if the transformation from grid to coordinate system in
* the GridCoverage is not an AffineTransform
*/
public void paint(
final Graphics2D graphics,
final GridCoverage2D gridCoverage,
final RasterSymbolizer symbolizer)
throws FactoryException, TransformException,
NoninvertibleTransformException {
//
// Initial checks
//
if(graphics==null)
throw new NullPointerException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1,"graphics"));
if(gridCoverage==null)
throw new NullPointerException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1,"gridCoverage"));
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuilder("Drawing coverage ").append(gridCoverage.toString()).toString());
final RenderingHints oldHints = graphics.getRenderingHints();
graphics.setRenderingHints(this.hints);
// Build the final image and the transformation
GCpair couple = prepareFinalImage(gridCoverage, symbolizer);
if (couple == null)
return;
RenderedImage finalImage = couple.getGridCoverage().getRenderedImage();
AffineTransform clonedFinalWorldToGrid = couple.getTransform();
try {
//debug
if (DEBUG) {
writeRenderedImage(finalImage,"final");
}
// //
// Drawing the Image
// //
graphics.drawRenderedImage(finalImage, clonedFinalWorldToGrid);
} catch (Throwable t) {
try {
//log the error
if(LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,t.getLocalizedMessage(),t);
// /////////////////////////////////////////////////////////////
// this is a workaround for a bug in Java2D, we need to convert
// the image to component color model to make it work just fine.
// /////////////////////////////////////////////////////////////
if(t instanceof IllegalArgumentException){
if (DEBUG) {
writeRenderedImage(finalImage,"preWORKAROUND1");
}
final RenderedImage image = new ImageWorker(finalImage).forceComponentColorModel(true).getRenderedImage();
if (DEBUG) {
writeRenderedImage(image,"WORKAROUND1");
}
graphics.drawRenderedImage(image, clonedFinalWorldToGrid);
}
else if(t instanceof ImagingOpException)
// /////////////////////////////////////////////////////////////
// this is a workaround for a bug in Java2D
// (see bug 4723021
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4723021).
//
// AffineTransformOp.filter throws a
// java.awt.image.ImagingOpException: Unable to tranform src
// image when a PixelInterleavedSampleModel is used.
//
// CUSTOMER WORKAROUND :
// draw the BufferedImage into a buffered image of type ARGB
// then perform the affine transform. THIS OPERATION WASTES
// RESOURCES BY PERFORMING AN ALLOCATION OF MEMORY AND A COPY ON
// LARGE IMAGES.
// /////////////////////////////////////////////////////////////
{
BufferedImage buf =
finalImage.getColorModel().hasAlpha()?
new BufferedImage(finalImage.getWidth(), finalImage.getHeight(), BufferedImage.TYPE_4BYTE_ABGR):
new BufferedImage(finalImage.getWidth(), finalImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
if (DEBUG) {
writeRenderedImage(buf,"preWORKAROUND2");
}
final Graphics2D g = (Graphics2D) buf.getGraphics();
final int translationX=finalImage.getMinX(),translationY=finalImage.getMinY();
g.drawRenderedImage(finalImage, AffineTransform.getTranslateInstance(-translationX, -translationY));
g.dispose();
if (DEBUG) {
writeRenderedImage(buf,"WORKAROUND2");
}
clonedFinalWorldToGrid.concatenate(AffineTransform.getTranslateInstance(translationX, translationY));
graphics.drawImage(buf, clonedFinalWorldToGrid, null);
//release
buf.flush();
buf=null;
}
else
//log the error
if(LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Unable to renderer this raster, no workaround found");
} catch (Throwable t1) {
// if the workaround fails again, there is really nothing to do
// :-(
LOGGER.log(Level.WARNING, t1.getLocalizedMessage(), t1);
}
}
// ///////////////////////////////////////////////////////////////////
//
// Restore old elements
//
// ///////////////////////////////////////////////////////////////////
graphics.setRenderingHints(oldHints);
}
private static Rectangle2D layoutHelper(RenderedImage source,
float scaleX,
float scaleY,
float transX,
float transY,
Interpolation interp) {
// Represent the scale factors as Rational numbers.
// Since a value of 1.2 is represented as 1.200001 which
// throws the forward/backward mapping in certain situations.
// Convert the scale and translation factors to Rational numbers
Rational scaleXRational = Rational.approximate(scaleX,rationalTolerance);
Rational scaleYRational = Rational.approximate(scaleY,rationalTolerance);
long scaleXRationalNum = (long) scaleXRational.num;
long scaleXRationalDenom = (long) scaleXRational.denom;
long scaleYRationalNum = (long) scaleYRational.num;
long scaleYRationalDenom = (long) scaleYRational.denom;
Rational transXRational = Rational.approximate(transX,rationalTolerance);
Rational transYRational = Rational.approximate(transY,rationalTolerance);
long transXRationalNum = (long) transXRational.num;
long transXRationalDenom = (long) transXRational.denom;
long transYRationalNum = (long) transYRational.num;
long transYRationalDenom = (long) transYRational.denom;
int x0 = source.getMinX();
int y0 = source.getMinY();
int w = source.getWidth();
int h = source.getHeight();
// Variables to store the calculated destination upper left coordinate
long dx0Num, dx0Denom, dy0Num, dy0Denom;
// Variables to store the calculated destination bottom right
// coordinate
long dx1Num, dx1Denom, dy1Num, dy1Denom;
// Start calculations for destination
dx0Num = x0;
dx0Denom = 1;
dy0Num = y0;
dy0Denom = 1;
// Formula requires srcMaxX + 1 = (x0 + w - 1) + 1 = x0 + w
dx1Num = x0 + w;
dx1Denom = 1;
// Formula requires srcMaxY + 1 = (y0 + h - 1) + 1 = y0 + h
dy1Num = y0 + h;
dy1Denom = 1;
dx0Num *= scaleXRationalNum;
dx0Denom *= scaleXRationalDenom;
dy0Num *= scaleYRationalNum;
dy0Denom *= scaleYRationalDenom;
dx1Num *= scaleXRationalNum;
dx1Denom *= scaleXRationalDenom;
dy1Num *= scaleYRationalNum;
dy1Denom *= scaleYRationalDenom;
// Equivalent to subtracting 0.5
dx0Num = 2 * dx0Num - dx0Denom;
dx0Denom *= 2;
dy0Num = 2 * dy0Num - dy0Denom;
dy0Denom *= 2;
// Equivalent to subtracting 1.5
dx1Num = 2 * dx1Num - 3 * dx1Denom;
dx1Denom *= 2;
dy1Num = 2 * dy1Num - 3 * dy1Denom;
dy1Denom *= 2;
// Adding translation factors
// Equivalent to float dx0 += transX
dx0Num = dx0Num * transXRationalDenom + transXRationalNum * dx0Denom;
dx0Denom *= transXRationalDenom;
// Equivalent to float dy0 += transY
dy0Num = dy0Num * transYRationalDenom + transYRationalNum * dy0Denom;
dy0Denom *= transYRationalDenom;
// Equivalent to float dx1 += transX
dx1Num = dx1Num * transXRationalDenom + transXRationalNum * dx1Denom;
dx1Denom *= transXRationalDenom;
// Equivalent to float dy1 += transY
dy1Num = dy1Num * transYRationalDenom + transYRationalNum * dy1Denom;
dy1Denom *= transYRationalDenom;
// Get the integral coordinates
int l_x0, l_y0, l_x1, l_y1;
l_x0 = Rational.ceil(dx0Num, dx0Denom);
l_y0 = Rational.ceil(dy0Num, dy0Denom);
l_x1 = Rational.ceil(dx1Num, dx1Denom);
l_y1 = Rational.ceil(dy1Num, dy1Denom);
// Set the top left coordinate of the destination
final Rectangle2D retValue= new Rectangle2D.Double();
retValue.setFrame(l_x0, l_y0, l_x1 - l_x0 + 1, l_y1 - l_y0 + 1);
return retValue;
}
}