/*
* 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.coverageio.jp2k;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
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 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.io.OverviewPolicy;
import org.geotools.coverageio.jp2k.RasterManager.OverviewLevel;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
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.operation.transform.AffineTransform2D;
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;
import com.sun.media.jai.codecimpl.util.ImagingException;
/**
* A RasterLayerResponse. An instance of this class is produced everytime a
* requestCoverage is called to a reader.
*
* @author Daniele Romagnoli, GeoSolutions S.A.S.
* @author Simone Giannecchini, GeoSolutions S.A.S.
*/
@SuppressWarnings("deprecation")
class RasterLayerResponse {
/**
*
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
class GranuleLoader implements Callable<RenderedImage> {
final ReferencedEnvelope cropBBox;
final MathTransform2D worldToGrid;
final Granule granule;
final ImageReadParam readParameters;
final int imageIndex;
final Dimension tilesDimension;
GranuleLoader(
final ImageReadParam readParameters,
final int imageIndex,
final ReferencedEnvelope cropBBox,
final MathTransform2D worldToGrid,
final Granule granule,
final Dimension tilesDimension) {
super();
this.readParameters = Utils.cloneImageReadParam(readParameters);
this.imageIndex = imageIndex;
this.cropBBox = cropBBox;
this.worldToGrid = worldToGrid;
this.granule = granule;
this.tilesDimension = tilesDimension != null ? (Dimension) tilesDimension.clone() : null;
}
public BoundingBox getCropBBox() {
return cropBBox;
}
public MathTransform2D getWorldToGrid() {
return worldToGrid;
}
public Granule getGranule() {
return granule;
}
public ImageReadParam getReadParameters() {
return readParameters;
}
public int getImageIndex() {
return imageIndex;
}
public RenderedImage call() throws Exception {
return granule.loadRaster(readParameters, imageIndex, cropBBox, worldToGrid, request, tilesDimension);
}
}
class GranuleWorker {
/**
* Default {@link Constructor}
*/
public GranuleWorker() {
}
private final List<Future<RenderedImage>> tasks = new ArrayList<Future<RenderedImage>>();
private int granulesNumber;
private boolean doInputTransparency;
private Color inputTransparentColor;
public void init(final ReferencedEnvelope aoi) {
// Get location and envelope of the image to load.
final ReferencedEnvelope granuleBBox = aoi;
// Load a granule from disk as requested.
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("About to read image number " + granulesNumber);
}
// If the granule is not there, dump a message and continue
final File rasterFile = new File(location);
if (rasterFile == null) {
return;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("File found " + location);
}
// granule cache
Granule granule = null;
synchronized (rasterManager.granulesCache) {
if (rasterManager.granulesCache.containsKey(rasterFile.toURI().toString())) {
granule = rasterManager.granulesCache.get(rasterFile.toURI().toString());
} else {
granule = new Granule(granuleBBox, rasterFile);
rasterManager.granulesCache.put(rasterFile.toURI().toString(), granule);
}
}
//
// load raster data
//
// create a granule loader
final GranuleLoader loader = new GranuleLoader(
baseReadParameters, imageChoice, bbox, finalWorldToGridCorner, granule, request.getTileDimensions());
tasks.add(new FutureTask<RenderedImage>(loader));
granulesNumber++;
}
public void produce() {
// reusable parameters
int granuleIndex = 0;
inputTransparentColor = request.getInputTransparentColor();
doInputTransparency = inputTransparentColor != null;
// execute them all
boolean firstGranule = true;
for (Future<RenderedImage> future : tasks) {
final RenderedImage loadedImage;
try {
// run the loading in this thread
final FutureTask<RenderedImage> task = (FutureTask<RenderedImage>) future;
task.run();
loadedImage = future.get();
if (loadedImage == null) {
if (LOGGER.isLoggable(Level.FINE)){
LOGGER.log(Level.FINE, new StringBuilder("Unable to load the raster for granule ")
.append(granuleIndex).append(" with request ").append(request.toString()).toString());
}
continue;
}
if (firstGranule) {
//
// 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();
firstGranule = false;
}
} catch (InterruptedException e) {
if (LOGGER.isLoggable(Level.SEVERE)){
LOGGER.log(Level.SEVERE, "Unable to load the raster for granule " + granuleIndex, e);
}
continue;
} catch (ExecutionException e) {
if (LOGGER.isLoggable(Level.SEVERE)){
LOGGER.log(Level.SEVERE, "Unable to load the raster for granule " + granuleIndex, e);
}
continue;
} catch (ImagingException e) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(new StringBuilder("Loading image number ").append(granuleIndex)
.append(" failed, original request was ").append(request).toString());
}
continue;
} catch (javax.media.jai.util.ImagingException e) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(new StringBuilder("Loading image number ").append(granuleIndex)
.append(" failed, original request was ").append(request).toString());
}
continue;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Loading image number " + granuleIndex);
}
final RenderedImage raster = processGranuleRaster(
loadedImage, granuleIndex, alphaIn, doInputTransparency, inputTransparentColor);
theImage = raster;
// increment index
granuleIndex++;
}
granulesNumber = granuleIndex;
if (granulesNumber == 0) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Unable to load any granule ");
}
return;
}
}
}
/** 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 URL inputURL;
private boolean frozen = false;
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;
/**
* 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;
inputURL = rasterManager.getInputURL();
File tempFile = null;
try {
if (inputURL.getProtocol().equalsIgnoreCase("file")) {
tempFile = new File(URLDecoder.decode(inputURL.getFile(), "UTF-8"));
} else {
throw new IllegalArgumentException("unsupported input:" + inputURL.toString());
}
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
location = tempFile.getAbsolutePath();
coverageEnvelope = rasterManager.getCoverageEnvelope();
this.coverageFactory = rasterManager.getCoverageFactory();
this.rasterManager = rasterManager;
baseGridToWorld = rasterManager.getRaster2Model();
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 synchronized void processRequest() throws IOException {
if (request.isEmpty()) {
throw new IOException("Empty request " + request.toString());
}
if (frozen) {
return;
}
// assemble granules
final RenderedImage image = prepareResponse();
RenderedImage finalRaster = postProcessRaster(image);
// create the coverage
gridCoverage = prepareCoverage(finalRaster);
// freeze
frozen = true;
}
/**
* 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 {
// select the relevant overview, notice that at this time we have
// relaxed a bit the requirement to have the same exact resolution
// for all the overviews, but still we do not allow for reading the
// various grid to world transform directly from the input files,
// therefore we are assuming that each granule has a scale and
// translate only grid to world that can be deduced from its base
// level dimension and envelope. 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(new StringBuilder("Loading level ").append(imageChoice)
.append(" with subsampling factors ")
.append(baseReadParameters.getSourceXSubsampling()).append(" ")
.append(baseReadParameters.getSourceYSubsampling()).toString());
}
final BoundingBox cropBBOX = request.getCropBBox();
if (cropBBOX != null) {
bbox = ReferencedEnvelope.reference(cropBBOX);
} else {
bbox = new ReferencedEnvelope(coverageEnvelope);
}
// 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(Utils.CENTER_TO_CORNER);
// keep into account overviews and subsampling
final OverviewLevel level = rasterManager.overviewsController.resolutionsLevels.get(imageChoice);
final OverviewLevel baseLevel = rasterManager.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();// compute raster bounds
rasterBounds = new GeneralGridEnvelope(
CRS.transform( finalWorldToGridCorner, bbox), PixelInCell.CELL_CORNER, false).toRectangle();
// create Init the granuleWorker
final GranuleWorker worker = new GranuleWorker();
worker.init(new ReferencedEnvelope(coverageEnvelope));
worker.produce();
//
// Did we actually load anything??
//
if (worker.granulesNumber >= 1) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(new StringBuilder(
"Loaded bbox ").append(bbox.toString())
.append(" while crop bbox ")
.append(request.getCropBBox().toString())
.toString());
}
return theImage;
} else {
// if we get here that means that we do not have anything to
// load
// but still we are inside the definition area
// we don't have background values available
return ConstantDescriptor.create(
Float.valueOf(rasterBounds.width),
Float.valueOf(rasterBounds.height),
new Byte[] { 0 }, this.rasterManager.getHints());
}
} catch (IOException e) {
throw new DataSourceException("Unable to create this image", e);
} catch (TransformException e) {
throw new DataSourceException("Unable to create this image", e);
}
}
private RenderedImage processGranuleRaster(RenderedImage granule,
final int granuleIndex, 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.
//
// 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 image 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 number " + granuleIndex);
}
granule = Utils.makeColorTransparent(transparentColor, granule);
}
return granule;
}
private GridCoverage2D prepareCoverage(RenderedImage image) throws IOException {
// creating bands
final SampleModel sm = image.getSampleModel();
final ColorModel cm = image.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);
}
return coverageFactory.create(rasterManager.getCoverageIdentifier(), image, new GeneralEnvelope(bbox), bands, null, null);
}
/**
* 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 = rasterManager.overviewsController.pickOverviewLevel(overviewPolicy, request);
// DECIMATION ON READING
rasterManager.decimationController.performDecimation(imageChoice, readParams, request);
return imageChoice;
}
private RenderedImage postProcessRaster(RenderedImage image) {
// alpha on the final 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;
}
}