/*
* 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.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.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;
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.
*/
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 = ImageUtilities.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 (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,"Unable to load the raster for granule " +granuleIndex+ " with request "+request.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("Loading image number " + granuleIndex+ " failed, original request was "+request);
continue;
}
catch (javax.media.jai.util.ImagingException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Loading image number " + granuleIndex+ " failed, original request was "+request);
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 data ");
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 = assembleGranules();
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 assembleGranules() 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 StringBuffer("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(CoverageUtilities.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 = ImageUtilities.maskColor(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;
}
}