/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2014, 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.coverage.io.netcdf;
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.RenderedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.media.jai.BorderExtender;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.TileCache;
import javax.media.jai.TileScheduler;
import org.geotools.coverage.GridSampleDimension;
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.grid.io.DimensionDescriptor;
import org.geotools.coverage.io.CoverageReadRequest;
import org.geotools.coverage.io.CoverageResponse;
import org.geotools.coverage.io.SpatialRequestHelper.CoverageProperties;
import org.geotools.coverage.io.impl.DefaultGridCoverageResponse;
import org.geotools.coverage.io.range.FieldType;
import org.geotools.coverage.io.range.RangeType;
import org.geotools.data.DataSourceException;
import org.geotools.data.Query;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.ImageWorker;
import org.geotools.imageio.netcdf.utilities.NetCDFUtilities;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.resources.image.ImageUtilities;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.geotools.util.Range;
import org.geotools.util.Utilities;
import org.opengis.coverage.SampleDimension;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.datum.PixelInCell;
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 Simone Giannecchini, GeoSolutions
* @author Daniele Romagnoli, GeoSolutions
* @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs
*/
@SuppressWarnings("rawtypes")
class NetCDFResponse extends CoverageResponse{
private final static double EPS = 1E-6;
private final static double[] DEFAULT_BACKGROUND_VALUES = new double[]{0d};
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(NetCDFResponse.class);
/** The {@link NetCDFRequest} originating this response */
private NetCDFRequest request;
/** The coverage factory producing a {@link GridCoverage} from an image */
final private static GridCoverageFactory COVERAGE_FACTORY = new GridCoverageFactory();
/** The base envelope related to the input coverage */
private GeneralEnvelope coverageEnvelope;
private ReferencedEnvelope targetBBox;
private Rectangle rasterBounds;
private MathTransform2D finalGridToWorldCorner;
private MathTransform2D finalWorldToGridCorner;
private URL datasetURL;
private ImageReadParam baseReadParameters = new ImageReadParam();
private boolean oversampledRequest;
private AffineTransform baseGridToWorld;
private Hints hints;
private AffineTransform targetWorldToGrid;
/**
* 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 COVERAGE_FACTORY a {@code GridCoverageFactory} to produce a {@code GridCoverage} when calling the {@link #createResponse()} method.
* @param readerSpi the Image Reader Service provider interface.
*/
public NetCDFResponse(final NetCDFRequest request) {
this.request = request;
CoverageReadRequest readRequest = request.originalRequest;
setRequest(readRequest);
datasetURL = (URL) request.source.reader.getInput();
}
public CoverageResponse createResponse() throws IOException {
processRequest();
return this;
}
/**
* 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 {
// is this query empty?
if (request.spatialRequestHelper.isEmpty()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Request is empty: " + request.toString());
}
return;
}
// assemble granules
prepareParams();
String timeFilterAttribute = null;
String elevationFilterAttribute = null;
CoverageReadRequest readRequest = (CoverageReadRequest) getRequest();
RangeType rangeType = request.source.getRangeType(null);
List<DimensionDescriptor> dimensionDescriptors = request.source.getDimensionDescriptors();
for (DimensionDescriptor dimensionDescriptor : dimensionDescriptors) {
if (dimensionDescriptor.getName().equalsIgnoreCase(NetCDFUtilities.ELEVATION_DIM)) {
// TODO Update this with ranged attributes
elevationFilterAttribute = dimensionDescriptor.getStartAttribute();
} else if (dimensionDescriptor.getName().equalsIgnoreCase(NetCDFUtilities.TIME_DIM)) {
// TODO Update this with ranged attributes
timeFilterAttribute = dimensionDescriptor.getStartAttribute();
}
}
Set<DateRange> temporalSubset = readRequest.getTemporalSubset();
Set<NumberRange<Double>> verticalSubset = readRequest.getVerticalSubset();
RangeType requestedRange = readRequest.getRangeSubset();
Set<FieldType> fieldTypes = requestedRange.getFieldTypes();
//
// adding GridCoverages to the results list
//
// //
Set<SampleDimension> sampleDims = null;
for (FieldType fieldType : fieldTypes) {
final Name name = fieldType.getName();
sampleDims = fieldType.getSampleDimensions();
if (rangeType != null) {
final FieldType ft = rangeType.getFieldType(name.getLocalPart());
if (ft != null)
sampleDims = ft.getSampleDimensions();
}
}
final GridSampleDimension[] sampleDimensions = sampleDims
.toArray(new GridSampleDimension[sampleDims.size()]);
// Forcing creation of subsets (even with a single null element)
Set<DateRange> tempSubset = null;
if (!temporalSubset.isEmpty()) {
tempSubset = temporalSubset;
} else {
tempSubset = new HashSet<DateRange>();
tempSubset.add(null);
}
Set<NumberRange<Double>> vertSubset = null;
if (!verticalSubset.isEmpty()) {
vertSubset = verticalSubset;
} else {
vertSubset = new HashSet<NumberRange<Double>>();
vertSubset.add(null);
}
Map<String, Set<?>> domainsSubset = readRequest.getAdditionalDomainsSubset();
Filter requestFilter = request.originalRequest.getFilter();
// handling date and time
for (DateRange timeRange : tempSubset) {
for (NumberRange<Double> elevation : vertSubset) {
Query query = new Query();
// handle time and elevation
createTimeElevationQuery(timeRange, elevation, query, requestFilter,
timeFilterAttribute, elevationFilterAttribute);
// handle additional params
additionalParamsManagement(query, domainsSubset, dimensionDescriptors);
// bbox
query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(),
FeatureUtilities.DEFAULT_FILTER_FACTORY.bbox(
FeatureUtilities.DEFAULT_FILTER_FACTORY.property("the_geom"),
targetBBox)));
query.setTypeName(request.name);
List<Integer> indexes = request.source.reader.getImageIndex(query);
if (indexes == null || indexes.isEmpty()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(" No indexes found for this query: " + query.toString());
}
continue;
}
int imageIndex = indexes.get(0);
final RenderedImage image = loadRaster(baseReadParameters, imageIndex, targetBBox,
finalWorldToGridCorner, hints);
// postproc
RenderedImage finalRaster = postProcessRaster(image);
// create the coverage
GridCoverage2D gridCoverage = prepareCoverage(finalRaster, sampleDimensions);
// Adding coverage domain
if (gridCoverage != null) {
GridCoverage gcResponse = new DefaultGridCoverageResponse(gridCoverage,
timeRange, elevation);
addResult(gcResponse);
}
}
}
// success
setStatus(Status.SUCCESS);
}
/**
* @param query
* @param domainsSubset
*/
private void additionalParamsManagement(Query query, Map<String, Set<?>> domainsSubset, List<DimensionDescriptor> dimensionDescriptors) {
if (domainsSubset.isEmpty()){
return;
}
Filter filter = query.getFilter();
for(Entry<String, Set<?>> entry:domainsSubset.entrySet()){
Set<?> values = entry.getValue();
String attribute = null;
for (DimensionDescriptor dim: dimensionDescriptors) {
if (dim.getName().toUpperCase().equalsIgnoreCase(entry.getKey())) {
attribute = dim.getStartAttribute();
break;
}
}
for(Object value:values){
if(value instanceof Range){
throw new UnsupportedOperationException();
} else {
filter=FeatureUtilities.DEFAULT_FILTER_FACTORY.and(filter,
FeatureUtilities.DEFAULT_FILTER_FACTORY.equals(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),
FeatureUtilities.DEFAULT_FILTER_FACTORY.literal(value)));
}
}
}
query.setFilter(filter);
}
/** Create the query to retrive the imageIndex related to the specified time (if any)
* and the specified elevation (if any)
* @param time
* @param elevation
* @param query
* @param requestFilter
* @param elevationFilterAttribute
* @param timeFilterAttribute
* @return
*/
private void createTimeElevationQuery(
DateRange time,
NumberRange<Double> elevation,
Query query,
Filter requestFilter, String timeFilterAttribute, String elevationFilterAttribute) {
final List<Filter> filters = new ArrayList<Filter>();
// //
// Setting up time filter
// //
if (time != null) {
final Range range = (Range) time;
// schema with only one time attribute. Consider adding code for schema with begin,end attributes
filters.add(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(
FeatureUtilities.DEFAULT_FILTER_FACTORY.lessOrEqual(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(timeFilterAttribute),
FeatureUtilities.DEFAULT_FILTER_FACTORY.literal(range.getMaxValue())),
FeatureUtilities.DEFAULT_FILTER_FACTORY.greaterOrEqual(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(timeFilterAttribute),
FeatureUtilities.DEFAULT_FILTER_FACTORY.literal(range.getMinValue()))));
}
// //
// Setting up elevation filter
// //
if (elevation != null) {
final Range range = (Range) elevation;
// schema with only one elevation attribute. Consider adding code for schema with begin,end attributes
filters.add(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(
FeatureUtilities.DEFAULT_FILTER_FACTORY.lessOrEqual(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(elevationFilterAttribute),
FeatureUtilities.DEFAULT_FILTER_FACTORY.literal(range.getMaxValue())),
FeatureUtilities.DEFAULT_FILTER_FACTORY.greaterOrEqual(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(elevationFilterAttribute),
FeatureUtilities.DEFAULT_FILTER_FACTORY.literal(range.getMinValue()))));
}
if (requestFilter != null) {
filters.add(requestFilter);
}
Filter filter = FeatureUtilities.DEFAULT_FILTER_FACTORY.and(filters);
query.setFilter(filter);
}
private RenderedImage postProcessRaster(RenderedImage image) {
// alpha on the final mosaic
if (!request.spatialRequestHelper.isNeedsReprojection()) {
//
// Check and see if the affine transform is doing a copy.
// If so call the copy operation.
//
// we are in raster space here, so 1E-3 is safe
if (XAffineTransform.isIdentity(targetWorldToGrid, EPS))
return image;
// create final image
//
// In case we are asked to use certain tile dimensions we tile
// also at this stage in case the read type is Direct since
// buffered images comes up untiled and this can affect the
// performances of the subsequent affine operation.
//
final Hints localHints = new Hints(hints);
if (hints != null && !hints.containsKey(JAI.KEY_BORDER_EXTENDER)) {
final Object extender = hints.get(JAI.KEY_BORDER_EXTENDER);
if (!(extender != null && extender instanceof BorderExtender)) {
localHints.add(ImageUtilities.EXTEND_BORDER_BY_COPYING);
}
}
ImageWorker iw = new ImageWorker(image);
iw.setRenderingHints(localHints);
iw.affine(targetWorldToGrid, request.getInterpolation(), DEFAULT_BACKGROUND_VALUES);
image = iw.getRenderedImage();
}
return image;
}
private void prepareParams() throws DataSourceException {
try {
baseReadParameters = new ImageReadParam();
performDecimation(baseReadParameters);
// === extract bbox
initBBOX();
// === init transformations
initTransformations();
// === init raster bounds
initRasterBounds();
// === init targetGrid2World
initTargetTransformation();
} catch (Exception e) {
throw new DataSourceException("Unable to create this mosaic", e);
}
}
private void initTargetTransformation() throws NoninvertibleTransformException {
// creating source grid to world corrected to the pixel corner
final AffineTransform sourceGridToWorld = new AffineTransform(
(AffineTransform) finalGridToWorldCorner);
// AffineTransform finalGridToWorldCorner = new AffineTransform((AffineTransform) finalGridToWorldCorner);
// target world to grid at the corner
final AffineTransform targetGridToWorld = new AffineTransform(
request.spatialRequestHelper.getRequestedGridToWorld());
targetGridToWorld.concatenate(CoverageUtilities.CENTER_TO_CORNER);
// target world to grid at the corner
targetWorldToGrid = targetGridToWorld.createInverse();
// final complete transformation
targetWorldToGrid.concatenate(sourceGridToWorld);
// update final grid to world
finalGridToWorldCorner = new AffineTransform2D(targetGridToWorld);
}
/**
* This method is responsible for computing the raster bounds of the final mosaic.
*
* @throws TransformException In case transformation fails during the process.
*/
private void initRasterBounds() throws TransformException {
final GeneralEnvelope tempRasterBounds = CRS.transform(finalWorldToGridCorner, targetBBox);
rasterBounds = tempRasterBounds.toRectangle2D().getBounds();
// SG using the above may lead to problems since the reason is that may be a little (1 px) bigger
// than what we need. The code below is a bit better since it uses a proper logic (see GridEnvelope
// Javadoc)
// rasterBounds = new GridEnvelope2D(new Envelope2D(tempRasterBounds), PixelInCell.CELL_CORNER);
if (rasterBounds.width == 0)
rasterBounds.width++;
if (rasterBounds.height == 0)
rasterBounds.height++;
if (oversampledRequest)
rasterBounds.grow(2, 2);
}
/**
* This method is responsible for initializing transformations g2w and back
*
* @throws Exception in case we don't manage to instantiate some of them.
*
*/
private void initTransformations() throws Exception {
//compute final world to grid
// base grid to world for the center of pixels
final AffineTransform g2w;
CoverageProperties properties = request.spatialRequestHelper.getCoverageProperties();
baseGridToWorld = (AffineTransform) properties.getGridToWorld2D();
double[] coverageFullResolution = properties.getFullResolution();
final double resX = coverageFullResolution[0];
final double resY = coverageFullResolution[1];
final double[] requestRes = request.spatialRequestHelper.getRequestedResolution();
g2w = new AffineTransform((AffineTransform) baseGridToWorld);
g2w.concatenate(CoverageUtilities.CENTER_TO_CORNER);
if ((requestRes[0] < resX || requestRes[1] < resY)) {
// Using the best available resolution
oversampledRequest = true;
} else {
// SG going back to working on a per level basis to do the composition
// g2w = new AffineTransform(request.getRequestedGridToWorld());
g2w.concatenate(AffineTransform.getScaleInstance(baseReadParameters.getSourceXSubsampling(), baseReadParameters.getSourceYSubsampling()));
}
// move it to the corner
finalGridToWorldCorner = new AffineTransform2D(g2w);
finalWorldToGridCorner = finalGridToWorldCorner.inverse();// compute raster bounds
}
/**
* This method is responsible for initializing the bbox for the
* mosaic produced by this response.
*
*/
private void initBBOX() {
// ok we got something to return, let's load records from the index
final BoundingBox cropBBOX = request.spatialRequestHelper.getCropBBox();
if (cropBBOX != null) {
targetBBox = ReferencedEnvelope.reference(cropBBOX);
} else {
targetBBox = new ReferencedEnvelope(coverageEnvelope);
}
}
/**
* This method is responsible for creating a coverage from the supplied {@link RenderedImage}.
*
* @param image
* @param sampleDimensions
* @return
* @throws IOException
*/
private GridCoverage2D prepareCoverage(RenderedImage image, GridSampleDimension[] sampleDimensions) throws IOException {
// creating the final coverage by keeping into account the fact that we
Map<String, String> properties = null;
// if (granulesPaths != null) {
// properties = new HashMap<String, String>();
// properties.put(AbstractGridCoverage2DReader.FILE_SOURCE_PROPERTY, granulesPaths);
// }
// image = TransposeDescriptor.create(image, TransposeDescriptor.FLIP_VERTICAL, hints);
return COVERAGE_FACTORY.create(request.name, image, new GridGeometry2D(new GridEnvelope2D(PlanarImage.wrapRenderedImage(image)
.getBounds()), PixelInCell.CELL_CORNER, finalGridToWorldCorner,
this.targetBBox.getCoordinateReferenceSystem(), hints), sampleDimensions, null,
properties);
}
/**
* Load a specified a raster as a portion of the granule describe by this {@link DefaultGranuleDescriptor}.
*
* @param imageReadParameters the {@link ImageReadParam} to use for reading.
* @param index the index to use for the {@link ImageReader}.
* @param cropBBox the bbox to use for cropping.
* @param mosaicWorldToGrid the cropping grid to world transform.
* @param request the incoming request to satisfy.
* @param hints {@link Hints} to be used for creating this raster.
* @return a specified a raster as a portion of the granule describe by this {@link DefaultGranuleDescriptor}.
* @throws IOException in case an error occurs.
*/
private RenderedImage loadRaster(final ImageReadParam imageReadParameters, final int index,
final ReferencedEnvelope cropBBox, final MathTransform2D mosaicWorldToGrid,
final Hints hints) throws IOException {
if (LOGGER.isLoggable(java.util.logging.Level.FINER)) {
final String name = Thread.currentThread().getName();
LOGGER.finer("Thread:" + name + " Loading raster data for granuleDescriptor "
+ this.toString());
}
ImageReadParam readParameters = null;
int imageIndex;
final ReferencedEnvelope bbox = request.spatialRequestHelper.getCoverageProperties().getBbox();
// intersection of this tile bound with the current crop bbox
final ReferencedEnvelope intersection = new ReferencedEnvelope(bbox.intersection(cropBBox),
cropBBox.getCoordinateReferenceSystem());
if (intersection.isEmpty()) {
if (LOGGER.isLoggable(java.util.logging.Level.FINE)) {
LOGGER.fine(new StringBuilder("Got empty intersection for granule ")
.append(this.toString()).append(" with request ")
.append(request.toString())
.append(" Resulting in no granule loaded: Empty result").toString());
}
return null;
}
try {
// What about thread safety?
imageIndex = index;
readParameters = imageReadParameters;
// now create the crop grid to world which can be used to decide
// which source area we need to crop in the selected level taking
// into account the scale factors imposed by the selection of this
// level together with the base level grid to world transformation
final AffineTransform gridToWorldTransform_ = new AffineTransform();
gridToWorldTransform_.preConcatenate(CoverageUtilities.CENTER_TO_CORNER);
gridToWorldTransform_.preConcatenate(baseGridToWorld);
AffineTransform2D cropWorldToGrid = new AffineTransform2D(gridToWorldTransform_);
cropWorldToGrid = (AffineTransform2D) cropWorldToGrid.inverse();
// computing the crop source area which lives into the
// selected level raster space, NOTICE that at the end we need to
// take into account the fact that we might also decimate therefore
// we cannot just use the crop grid to world but we need to correct
// it.
Rectangle sourceArea = CRS.transform(cropWorldToGrid, intersection)
.toRectangle2D().getBounds();
// Selection of the source original area for cropping the computed source area
// (may have negative values for the approximation)
final Rectangle initialArea = request.source.getSpatialDomain()
.getRasterElements(true, null).iterator().next().toRectangle();
sourceArea = sourceArea.intersection(initialArea);
if (sourceArea.isEmpty()) {
if (LOGGER.isLoggable(java.util.logging.Level.FINE)) {
LOGGER.fine("Got empty area for granuleDescriptor " + this.toString()
+ " with request " + request.toString()
+ " Resulting in no granule loaded: Empty result");
}
return null;
} else if (LOGGER.isLoggable(java.util.logging.Level.FINER)) {
LOGGER.finer("Loading level " + imageIndex + " with source region: " + sourceArea
+ " subsampling: " + readParameters.getSourceXSubsampling() + ","
+ readParameters.getSourceYSubsampling() + " for granule:" + datasetURL);
}
// set the source region
readParameters.setSourceRegion(sourceArea);
final RenderedImage raster;
try {
// read
raster = request.readType.read(readParameters, imageIndex, datasetURL,
request.spatialRequestHelper.getCoverageProperties().getRasterArea(),
request.source.reader, hints, false);
} catch (Throwable e) {
if (LOGGER.isLoggable(java.util.logging.Level.FINE)) {
LOGGER.log(java.util.logging.Level.FINE,
"Unable to load raster for granuleDescriptor " + this.toString()
+ " with request " + request.toString()
+ " Resulting in no granule loaded: Empty result", e);
}
return null;
}
// use fixed source area
sourceArea.setRect(readParameters.getSourceRegion());
//
// setting new coefficients to define a new affineTransformation
// to be applied to the grid to world transformation
// -----------------------------------------------------------------------------------
//
// With respect to the original envelope, the obtained planarImage
// needs to be rescaled. The scaling factors are computed as the
// ratio between the cropped source region sizes and the read
// image sizes.
//
// place it in the mosaic using the coords created above;
double decimationScaleX = ((1.0 * sourceArea.width) / raster.getWidth());
double decimationScaleY = ((1.0 * sourceArea.height) / raster.getHeight());
final AffineTransform decimationScaleTranform = XAffineTransform.getScaleInstance(
decimationScaleX, decimationScaleY);
// keep into account translation to work into the selected level raster space
final AffineTransform afterDecimationTranslateTranform = XAffineTransform
.getTranslateInstance(sourceArea.x, sourceArea.y);
// // now we need to go back to the base level raster space
// final AffineTransform backToBaseLevelScaleTransform =selectedlevel.baseToLevelTransform;
//
// now create the overall transform
final AffineTransform finalRaster2Model = new AffineTransform(baseGridToWorld);
finalRaster2Model.concatenate(CoverageUtilities.CENTER_TO_CORNER);
if (!XAffineTransform.isIdentity(afterDecimationTranslateTranform, EPS))
finalRaster2Model.concatenate(afterDecimationTranslateTranform);
if (!XAffineTransform.isIdentity(decimationScaleTranform, EPS))
finalRaster2Model.concatenate(decimationScaleTranform);
// keep into account translation factors to place this tile
finalRaster2Model.preConcatenate((AffineTransform) mosaicWorldToGrid);
final Interpolation interpolation = request.getInterpolation();
// paranoiac check to avoid that JAI freaks out when computing its internal layouT on images that are too small
Rectangle2D finalLayout = ImageUtilities.layoutHelper(raster,
(float) finalRaster2Model.getScaleX(), (float) finalRaster2Model.getScaleY(),
(float) finalRaster2Model.getTranslateX(),
(float) finalRaster2Model.getTranslateY(), interpolation);
if (finalLayout.isEmpty()) {
if (LOGGER.isLoggable(java.util.logging.Level.INFO))
LOGGER.info("Unable to create a granuleDescriptor " + this.toString()
+ " due to jai scale bug creating a null source area");
return null;
}
// apply the affine transform conserving indexed color model
final RenderingHints localHints = new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL,
interpolation instanceof InterpolationNearest ? Boolean.FALSE : Boolean.TRUE);
//
// In case we are asked to use certain tile dimensions we tile
// also at this stage in case the read type is Direct since
// buffered images comes up untiled and this can affect the
// performances of the subsequent affine operation.
//
// final Dimension tileDimensions=request.getTileDimensions();
// if(tileDimensions!=null&&request.getReadType().equals(ReadType.DIRECT_READ)) {
// final ImageLayout layout = new ImageLayout();
// layout.setTileHeight(tileDimensions.width).setTileWidth(tileDimensions.height);
// localHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT,layout));
// } else {
// if (hints != null && hints.containsKey(JAI.KEY_IMAGE_LAYOUT)) {
// final Object layout = hints.get(JAI.KEY_IMAGE_LAYOUT);
// if (layout != null && layout instanceof ImageLayout) {
// localHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, ((ImageLayout) layout).clone()));
// }
// }
// }
if (hints != null && hints.containsKey(JAI.KEY_TILE_CACHE)) {
final Object cache = hints.get(JAI.KEY_TILE_CACHE);
if (cache != null && cache instanceof TileCache)
localHints.add(new RenderingHints(JAI.KEY_TILE_CACHE, (TileCache) cache));
}
if (hints != null && hints.containsKey(JAI.KEY_TILE_SCHEDULER)) {
final Object scheduler = hints.get(JAI.KEY_TILE_SCHEDULER);
if (scheduler != null && scheduler instanceof TileScheduler)
localHints.add(new RenderingHints(JAI.KEY_TILE_SCHEDULER,
(TileScheduler) scheduler));
}
boolean addBorderExtender = true;
if (hints != null && hints.containsKey(JAI.KEY_BORDER_EXTENDER)) {
final Object extender = hints.get(JAI.KEY_BORDER_EXTENDER);
if (extender != null && extender instanceof BorderExtender) {
localHints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER,
(BorderExtender) extender));
addBorderExtender = false;
}
}
// border extender
if (addBorderExtender) {
localHints.add(ImageUtilities.BORDER_EXTENDER_HINTS);
}
ImageWorker iw = new ImageWorker(raster);
iw.setRenderingHints(localHints);
iw.affine(finalRaster2Model, interpolation, DEFAULT_BACKGROUND_VALUES);
return iw.getRenderedImage();
} catch (IllegalStateException e) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.log(java.util.logging.Level.WARNING,
new StringBuilder("Unable to load raster for granuleDescriptor ")
.append(this.toString()).append(" with request ")
.append(request.toString())
.append(" Resulting in no granule loaded: Empty result").toString(),
e);
}
return null;
} catch (org.opengis.referencing.operation.NoninvertibleTransformException e) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.log(java.util.logging.Level.WARNING,
new StringBuilder("Unable to load raster for granuleDescriptor ")
.append(this.toString()).append(" with request ")
.append(request.toString())
.append(" Resulting in no granule loaded: Empty result").toString(),
e);
}
return null;
} catch (TransformException e) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.log(java.util.logging.Level.WARNING,
new StringBuilder("Unable to load raster for granuleDescriptor ")
.append(this.toString()).append(" with request ")
.append(request.toString())
.append(" Resulting in no granule loaded: Empty result").toString(),
e);
}
return null;
} finally {
// try {
// if (request.getReadType() != ReadType.JAI_IMAGEREAD && inStream != null) {
// inStream.close();
// }
// } finally {
// if (request.getReadType() != ReadType.JAI_IMAGEREAD && reader != null) {
// reader.dispose();
// }
// }
}
}
/**
* This method is responsible for evaluating possible subsampling factors once the best resolution level has been found, in case we have support
* for overviews, or starting from the original coverage in case there are no overviews available.
*
* Anyhow this method should not be called directly but subclasses should make use of the setReadParams method instead in order to transparently
* look for overviews.
*
* @param levelIndex
* @param readParameters
* @param requestedRes
*/
private void performDecimation(ImageReadParam readParameters) {
final double[] requestedResolution = request.spatialRequestHelper.getRequestedResolution();
final Rectangle coverageRasterArea = request.spatialRequestHelper.getCoverageProperties().getRasterArea();
final double[] fullResolution = request.spatialRequestHelper.getCoverageProperties().getFullResolution();
// the read parameters cannot be null
Utilities.ensureNonNull("readParameters", readParameters);
// get the requested resolution
if (requestedResolution == null) {
// if there is no requested resolution we don't do any
// subsampling
readParameters.setSourceSubsampling(1, 1, 0, 0);
return;
}
final int rasterWidth, rasterHeight;
// highest resolution
rasterWidth = coverageRasterArea.width;
rasterHeight = coverageRasterArea.height;
// /////////////////////////////////////////////////////////////////////
// DECIMATION ON READING
// Setting subsampling factors with some checks
// 1) the subsampling factors cannot be zero
// 2) the subsampling factors cannot be such that the w or h are
// zero
// /////////////////////////////////////////////////////////////////////
int subSamplingFactorX = (int) Math.floor(requestedResolution[0] / fullResolution[0]);
subSamplingFactorX = subSamplingFactorX == 0 ? 1 : subSamplingFactorX;
while (rasterWidth / subSamplingFactorX <= 0 && subSamplingFactorX >= 0)
subSamplingFactorX--;
subSamplingFactorX = subSamplingFactorX <= 0 ? 1 : subSamplingFactorX;
int subSamplingFactorY = (int) Math.floor(requestedResolution[1] / fullResolution[1]);
subSamplingFactorY = subSamplingFactorY == 0 ? 1 : subSamplingFactorY;
while (rasterHeight / subSamplingFactorY <= 0 && subSamplingFactorY >= 0)
subSamplingFactorY--;
subSamplingFactorY = subSamplingFactorY <= 0 ? 1 : subSamplingFactorY;
readParameters.setSourceSubsampling(subSamplingFactorX, subSamplingFactorY, 0, 0);
}
}