/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.gce.imagecollection;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.media.jai.operator.ConstantDescriptor;
import javax.media.jai.util.ImagingException;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.TypeMap;
import org.geotools.coverage.grid.GeneralGridEnvelope;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.data.DataSourceException;
import org.geotools.factory.Hints;
import org.geotools.gce.imagecollection.RasterManager.OverviewLevel;
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.operation.transform.AffineTransform2D;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.resources.image.ImageUtilities;
import org.opengis.coverage.ColorInterpretation;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
/**
* A RasterLayerResponse. An instance of this class is produced everytime a
* requestCoverage is called to a reader.
*
* @author Daniele Romagnoli, GeoSolutions
*/
class RasterLayerResponse {
class GranuleWorker {
// private boolean doInputTransparency;
// private Color inputTransparentColor;
private RasterGranuleLoader rasterGranuleLoader;
/**
* Default {@link Constructor}
*
* @param aoi
* @param gridToWorldCorner
*/
public GranuleWorker(ReferencedEnvelope aoi,
MathTransform gridToWorldCorner) {
init(aoi, gridToWorldCorner);
}
private void init(final ReferencedEnvelope aoi, final MathTransform gridToWorld) {
// Get location and envelope of the image to load.
final ReferencedEnvelope granuleBBox = aoi;
// Load a rasterGranuleLoader from disk as requested.
// If the rasterGranuleLoader is not there, dump a message and continue
final File rasterFile = new File(location);
// rasterGranuleLoader creation
rasterGranuleLoader = new RasterGranuleLoader(granuleBBox, rasterFile, gridToWorld);
}
public void produce() {
// inputTransparentColor = request.getInputTransparentColor();
// doInputTransparency = inputTransparentColor != null;
// execute them all
RenderedImage loadedImage;
try {
loadedImage = rasterGranuleLoader.loadRaster(
baseReadParameters, imageChoice, bbox,
finalWorldToGridCorner, request,
request.getTileDimensions());
if (loadedImage == null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,
"Unable to load the raster with request "
+ request.toString());
}
//
// We check here if the images have an alpha channel or some
// other sort of transparency. In case we have transparency
// I also save the index of the transparent channel.
//
final ColorModel cm = loadedImage.getColorModel();
alphaIn = cm.hasAlpha();
} catch (ImagingException e) {
if (LOGGER.isLoggable(Level.INFO))
LOGGER.fine("Loading image failed, original request was " + request);
loadedImage = null;
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.INFO))
LOGGER.fine("Loading image failed, original request was " + request);
loadedImage = null;
}
if (loadedImage == null) {
if (LOGGER.isLoggable(Level.INFO))
LOGGER.log(Level.FINE, "Unable to load any data ");
return;
}
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Processing loaded raster data ");
// final RenderedImage raster = processGranuleRaster(loadedImage,
// granuleIndex, alphaIn, doInputTransparency,
// inputTransparentColor);
//
final RenderedImage raster = processGranuleRaster(loadedImage, alphaIn, false, null);
theImage = raster;
}
}
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(RasterLayerResponse.class);
/** The GridCoverage produced after a {@link #compute()} method call */
private GridCoverage2D gridCoverage;
/** The {@link RasterLayerRequest} originating this response */
private RasterLayerRequest request;
/** The coverage factory producing a {@link GridCoverage} from an image */
private GridCoverageFactory coverageFactory;
/** The base envelope related to the input coverage */
private GeneralEnvelope coverageEnvelope;
private RasterManager rasterManager;
// private Color transparentColor;
private RenderedImage theImage;
private ReferencedEnvelope bbox;
private Rectangle rasterBounds;
private MathTransform2D finalGridToWorldCorner;
private MathTransform2D finalWorldToGridCorner;
private int imageChoice = 0;
private ImageReadParam baseReadParameters = new ImageReadParam();
private boolean alphaIn = false;
private String location;
private MathTransform baseGridToWorld;
private double[] backgroundValues;
/**
* Construct a {@code RasterLayerResponse} given a specific
* {@link RasterLayerRequest}, a {@code GridCoverageFactory} to produce
* {@code GridCoverage}s and an {@code ImageReaderSpi} to be used for
* instantiating an Image Reader for a read operation,
*
* @param request
* a {@link RasterLayerRequest} originating this response.
* @param coverageFactory
* a {@code GridCoverageFactory} to produce a
* {@code GridCoverage} when calling the {@link #compute()}
* method.
* @param readerSpi
* the Image Reader Service provider interface.
*/
public RasterLayerResponse(final RasterLayerRequest request,
final RasterManager rasterManager) {
this.request = request;
location = request.imageManager.property.getPath();
coverageEnvelope = request.imageManager.coverageEnvelope;
baseGridToWorld = request.imageManager.coverageGridToWorld2D;
coverageFactory = rasterManager.getCoverageFactory();
this.rasterManager = rasterManager;
backgroundValues = request.getBackgroundValues();
// transparentColor = request.getInputTransparentColor();
}
/**
* Compute the coverage request and produce a grid coverage which will be
* returned by {@link #createResponse()}. The produced grid coverage may be
* {@code null} in case of empty request.
*
* @return the {@link GridCoverage} produced as computation of this response
* using the {@link #compute()} method.
* @throws IOException
* @uml.property name="gridCoverage"
*/
public GridCoverage2D createResponse() throws IOException {
processRequest();
return gridCoverage;
}
/**
* @return the {@link RasterLayerRequest} originating this response.
*
* @uml.property name="request"
*/
public RasterLayerRequest getOriginatingCoverageRequest() {
return request;
}
/**
* This method creates the GridCoverage2D from the underlying file given a
* specified envelope, and a requested dimension.
*
* @param iUseJAI
* specify if the underlying read process should leverage on a
* JAI ImageRead operation or a simple direct call to the
* {@code read} method of a proper {@code ImageReader}.
* @param overviewPolicy
* the overview policy which need to be adopted
* @return a {@code GridCoverage}
*
* @throws java.io.IOException
*/
private void processRequest() throws IOException {
if (request.isEmpty()) {
throw new DataSourceException("Empty request: " + request.toString());
} else if (request.imageManager.property.getPath().equalsIgnoreCase(Utils.FAKE_IMAGE_PATH)){
finalGridToWorldCorner = Utils.IDENTITY_2D_FLIP ;
//TODO Re-enable this when supportin y as DISPLAY_DOWN
// finalGridToWorldCorner = rasterManager.parent.defaultValues.epsgCode == 404001 ? Utils.IDENTITY_2D : Utils.IDENTITY_2D_FLIP ;
gridCoverage = prepareCoverage(Utils.DEFAULT_IMAGE);
return;
}
// assemble granules
final RenderedImage image = prepareResponse();
// RenderedImage finalRaster = postProcessRaster(image);
// create the coverage
gridCoverage = prepareCoverage(image);
}
/**
* This method loads the granules which overlap the requested
* {@link GeneralEnvelope} using the provided values for alpha and input
* ROI.
*/
private RenderedImage prepareResponse() throws DataSourceException {
try {
// final double[] backgroundValues = request.getBackgroundValues();
// select the relevant overview, notice that at this time we have
// The grid to world transforms for the other levels can be computed
// accordingly knowning the scale factors.
if (request.getRequestedBBox() != null && request.getRequestedRasterArea() != null)
imageChoice = setReadParams(request.getOverviewPolicy(), baseReadParameters, request);
else
imageChoice = 0;
assert imageChoice >= 0;
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Loading level " + imageChoice + "with subsampling factors "
+ baseReadParameters.getSourceXSubsampling() + " "
+ baseReadParameters.getSourceYSubsampling());
final BoundingBox cropBBOX = request.getCropBBox();
if (cropBBOX != null)
bbox = ReferencedEnvelope.reference(cropBBOX);
else
bbox = new ReferencedEnvelope(coverageEnvelope);
// XAffineTransform.getFlip((AffineTransform) baseGridToWorld);
// compute final world to grid
// base grid to world for the center of pixels
final AffineTransform g2w = new AffineTransform((AffineTransform) baseGridToWorld);
// move it to the corner
g2w.concatenate(CoverageUtilities.CENTER_TO_CORNER);
// keep into account overviews and subsampling
final OverviewLevel level = request.imageManager.overviewsController.resolutionsLevels
.get(imageChoice);
final OverviewLevel baseLevel = request.imageManager.overviewsController.resolutionsLevels.get(0);
final AffineTransform2D adjustments = new AffineTransform2D(
(level.resolutionX / baseLevel.resolutionX)
* baseReadParameters.getSourceXSubsampling(), 0, 0,
(level.resolutionY / baseLevel.resolutionY)
* baseReadParameters.getSourceYSubsampling(), 0, 0);
g2w.concatenate(adjustments);
finalGridToWorldCorner = new AffineTransform2D(g2w);
finalWorldToGridCorner = finalGridToWorldCorner.inverse();
rasterBounds = new GeneralGridEnvelope(CRS.transform(
finalWorldToGridCorner, bbox), PixelInCell.CELL_CORNER,
false).toRectangle();
final GranuleWorker worker = new GranuleWorker(
new ReferencedEnvelope(coverageEnvelope), baseGridToWorld);
worker.produce();
//
// Did we actually load anything?
//
if (theImage != null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Loaded bbox " + bbox.toString()
+ " while crop bbox " + request.getCropBBox());
return theImage;
} else {
if (backgroundValues == null) {
// we don't have background values available
return ConstantDescriptor.create(
Float.valueOf(rasterBounds.width),
Float.valueOf(rasterBounds.height),
new Byte[] { 0 }, this.rasterManager.getHints());
} else {
// we have background values available
final Double[] values = new Double[backgroundValues.length];
for (int i = 0; i < values.length; i++)
values[i] = backgroundValues[i];
return ConstantDescriptor.create(
Float.valueOf(rasterBounds.width),
Float.valueOf(rasterBounds.height), values,
this.rasterManager.getHints());
}
}
} catch (IOException e) {
throw new DataSourceException("Unable to create this response", e);
} catch (TransformException e) {
throw new DataSourceException("Unable to create this response", e);
}
}
private RenderedImage processGranuleRaster(RenderedImage granule, final boolean alphaIn,
final boolean doTransparentColor, final Color transparentColor) {
//
// INDEX COLOR MODEL EXPANSION
//
// Take into account the need for an expansions of the original color
// model.
//
// If the original color model is an index color model an expansion
// might be requested in case the different palettes are not all the
// same. In this case the mosaic operator from JAI would provide wrong
// results since it would take the first palette and use that one for
// all the other images.
//
// There is a special case to take into account here. In case the input
// images use an IndexColorModel it might happen that the transparent
// color is present in some of them while it is not present in some
// others. This case is the case where for sure a color expansion is
// needed. However we have to take into account that during the masking
// phase the images where the requested transparent color was present
// will have 4 bands, the other 3. If we want the mosaic to work we
// have to add an extra band to the latter type of images for providing
// alpha information to them.
//
//
if (rasterManager.expandMe && granule.getColorModel() instanceof IndexColorModel) {
granule = new ImageWorker(granule).forceComponentColorModel()
.getRenderedImage();
}
//
// TRANSPARENT COLOR MANAGEMENT
//
//
if (doTransparentColor) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Support for alpha on input image");
granule = ImageUtilities.maskColor(transparentColor, granule);
}
return granule;
}
private GridCoverage2D prepareCoverage(final RenderedImage data)
throws IOException {
// creating bands
final SampleModel sm = data.getSampleModel();
final ColorModel cm = data.getColorModel();
final int numBands = sm.getNumBands();
final GridSampleDimension[] bands = new GridSampleDimension[numBands];
// setting bands names.
for (int i = 0; i < numBands; i++) {
final ColorInterpretation colorInterpretation = TypeMap
.getColorInterpretation(cm, i);
if (colorInterpretation == null)
throw new IOException("Unrecognized sample dimension type");
bands[i] = new GridSampleDimension(colorInterpretation.name())
.geophysics(true);
}
// creating the final coverage by keeping into account the fact that we
// can just use the envelope, but we need to use the G2W
Map <String, String> properties = new HashMap<String,String>();
properties.put(AbstractGridCoverage2DReader.FILE_SOURCE_PROPERTY, location);
return coverageFactory.create(rasterManager.getCoverageIdentifier(),
data, new GridGeometry2D(new GeneralGridEnvelope(data, 2),
PixelInCell.CELL_CORNER, finalGridToWorldCorner,
this.rasterManager.getCoverageCRS(), null), bands,
null, properties);
}
/**
* This method is responsible for preparing the read param for doing an
* {@link ImageReader#read(int, ImageReadParam)}.
*
*
* <p>
* This method is responsible for preparing the read param for doing an
* {@link ImageReader#read(int, ImageReadParam)}. It sets the passed
* {@link ImageReadParam} in terms of decimation on reading using the
* provided requestedEnvelope and requestedDim to evaluate the needed
* resolution. It also returns and {@link Integer} representing the index of
* the raster to be read when dealing with multipage raster.
*
* @param overviewPolicy
* it can be one of {@link Hints#VALUE_OVERVIEW_POLICY_IGNORE},
* {@link Hints#VALUE_OVERVIEW_POLICY_NEAREST},
* {@link Hints#VALUE_OVERVIEW_POLICY_QUALITY} or
* {@link Hints#VALUE_OVERVIEW_POLICY_SPEED}. It specifies the
* policy to compute the overviews level upon request.
* @param readParams
* an instance of {@link ImageReadParam} for setting the
* subsampling factors.
* @param requestedEnvelope
* the {@link GeneralEnvelope} we are requesting.
* @param requestedDim
* the requested dimensions.
* @return the index of the raster to read in the underlying data source.
* @throws IOException
* @throws TransformException
*/
private int setReadParams(final OverviewPolicy overviewPolicy,
final ImageReadParam readParams, final RasterLayerRequest request)
throws IOException, TransformException {
// Default image index 0
int imageChoice = 0;
// default values for subsampling
readParams.setSourceSubsampling(1, 1, 0, 0);
//
// Init overview policy
//
// //
// when policy is explictly provided it overrides the policy provided
// using hints.
final OverviewPolicy policy;
if (overviewPolicy == null)
policy = rasterManager.overviewPolicy;
else
policy = overviewPolicy;
// requested to ignore overviews
if (policy.equals(OverviewPolicy.IGNORE))
return imageChoice;
// overviews and decimation
imageChoice = request.imageManager.overviewsController.pickOverviewLevel(
overviewPolicy, request);
// DECIMATION ON READING
rasterManager.decimationController.computeDecimationFactors(
imageChoice, readParams, request);
return imageChoice;
}
// private RenderedImage postProcessRaster(RenderedImage image) {
// if (transparentColor != null) {
// if (LOGGER.isLoggable(Level.FINE))
// LOGGER.fine("Support for alpha on final image");
// final ImageWorker w = new ImageWorker(image);
// if (image.getSampleModel() instanceof MultiPixelPackedSampleModel)
// w.forceComponentColorModel();
// return w.makeColorTransparent(transparentColor).getRenderedImage();
//
// }
// return image;
// }
}