/*
* 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.Dimension;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FilenameUtils;
import org.geotools.coverage.grid.GeneralGridEnvelope;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.data.DataSourceException;
import org.geotools.factory.Hints;
import org.geotools.filter.AttributeExpressionImpl;
import org.geotools.filter.IsEqualsToImpl;
import org.geotools.gce.imagecollection.RasterManager.ImageManager;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.metadata.iso.spatial.PixelTranslation;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.opengis.filter.Filter;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.expression.Literal;
import org.opengis.geometry.BoundingBox;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.Identifier;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.ReferenceIdentifier;
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.NoninvertibleTransformException;
import org.opengis.referencing.operation.TransformException;
/**
* A class to handle coverage requests to an {@link ImageCollectionReader} reader.
*
* @author Daniele Romagnoli, GeoSolutions
*/
class RasterLayerRequest {
private final static String PATH_KEY = "PATH";
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(RasterLayerRequest.class);
private ReadType readType = AbstractGridFormat.USE_JAI_IMAGEREAD
.getDefaultValue() ? ReadType.JAI_IMAGEREAD : ReadType.DIRECT_READ;
/** The {@link BoundingBox} requested */
private BoundingBox requestedBBox;
/**
* The {@link BoundingBox} of the portion of the coverage that intersects
* the requested bbox
*/
private BoundingBox cropBBox;
/** The desired overview Policy for this request */
private OverviewPolicy overviewPolicy;
/** The region where to fit the requested envelope */
private Rectangle requestedRasterArea;
/** The region of the */
private Rectangle destinationRasterArea;
/**
* Set to {@code true} if this request will produce an empty result, and the
* coverageResponse will produce a {@code null} coverage.
*/
private boolean empty;
private AffineTransform requestedGridToWorld;
private double[] requestedResolution;
private double[] backgroundValues;
private RasterManager rasterManager;
private double[] requestedRasterScaleFactors;
private Dimension tileDimensions;
private Filter filter = null;
ImageManager imageManager;
/**
* Build a new {@code CoverageRequest} given a set of input parameters.
*
* @param params
* The {@code GeneralParameterValue}s to initialize this request
* @param baseGridCoverage2DReader
* @throws DataSourceException
*/
public RasterLayerRequest(final GeneralParameterValue[] params,
final RasterManager rasterManager) throws DataSourceException {
// //
//
// Setting default parameters
//
// //
this.rasterManager = rasterManager;
setDefaultParameterValues();
// //
//
// Parsing parameter that can be used to control this request
//
// //
if (params != null) {
for (GeneralParameterValue gParam : params) {
if (gParam instanceof ParameterValue<?>) {
final ParameterValue<?> param = (ParameterValue<?>) gParam;
final ReferenceIdentifier name = param.getDescriptor().getName();
extractParameter(param, name);
}
}
}
// //
//
// Set specific imageIO parameters: type of read operation,
// imageReadParams
//
// //
checkReadType();
prepare();
}
private void setDefaultParameterValues() {
final ParameterValueGroup readParams = this.rasterManager.parent
.getFormat().getReadParameters();
if (readParams == null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("No default values for the read parameters!");
return;
}
final List<GeneralParameterDescriptor> parametersDescriptors = readParams
.getDescriptor().descriptors();
for (GeneralParameterDescriptor descriptor : parametersDescriptors) {
// we canc get the default vale only with the ParameterDescriptor
// class
if (!(descriptor instanceof ParameterDescriptor))
continue;
// get name and default value
final ParameterDescriptor desc = (ParameterDescriptor) descriptor;
final ReferenceIdentifier name = desc.getName();
final Object value = desc.getDefaultValue();
// //
//
// Requested GridGeometry2D parameter
//
// //
if (descriptor.getName().equals(
AbstractGridFormat.READ_GRIDGEOMETRY2D.getName())) {
if (value == null)
continue;
final GridGeometry2D gg = (GridGeometry2D) value;
requestedBBox = new ReferencedEnvelope((Envelope) gg.getEnvelope2D());
requestedRasterArea = gg.getGridRange2D().getBounds();
requestedGridToWorld = (AffineTransform) gg.getGridToCRS2D();
continue;
}
if (name.equals(ImageCollectionFormat.BACKGROUND_VALUES.getName())) {
if (value == null)
continue;
backgroundValues = (double[]) value;
continue;
}
// //
//
// Use JAI ImageRead parameter
//
// //
if (name.equals(AbstractGridFormat.USE_JAI_IMAGEREAD.getName())) {
if (value == null)
continue;
readType = ((Boolean) value) ? ReadType.JAI_IMAGEREAD
: ReadType.DIRECT_READ;
continue;
}
// //
//
// Overview Policy parameter
//
// //
if (name.equals(AbstractGridFormat.OVERVIEW_POLICY.getName())) {
if (value == null)
continue;
overviewPolicy = (OverviewPolicy) value;
continue;
}
if (name.equals(ImageCollectionFormat.SUGGESTED_TILE_SIZE.getName())) {
final String suggestedTileSize = (String) value;
// Preliminary checks on parameter value
if ((suggestedTileSize != null) && (suggestedTileSize.trim().length() > 0)) {
if (suggestedTileSize.contains(ImageCollectionFormat.TILE_SIZE_SEPARATOR)) {
final String[] tilesSize = suggestedTileSize.split(ImageCollectionFormat.TILE_SIZE_SEPARATOR);
if (tilesSize.length == 2) {
try {
// Getting suggested tile size
final int tileWidth = Integer.valueOf(tilesSize[0].trim());
final int tileHeight = Integer.valueOf(tilesSize[1].trim());
tileDimensions = new Dimension(tileWidth, tileHeight);
} catch (NumberFormatException nfe) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING,
"Unable to parse suggested tile size parameter");
}
}
}
}
}
}
}
}
/**
* Set proper fields from the specified input parameter.
*
* @param param
* the input {@code ParamaterValue} object
* @param name
* the name of the parameter
*/
private void extractParameter(ParameterValue<?> param, Identifier name) {
// //
//
// Requested GridGeometry2D parameter
//
// //
if (name.equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName())) {
final Object value = param.getValue();
if (value == null)
return;
final GridGeometry2D gg = (GridGeometry2D) param.getValue();
if (gg == null) {
return;
}
requestedBBox = new ReferencedEnvelope((Envelope) gg.getEnvelope2D());
requestedRasterArea = gg.getGridRange2D().getBounds();
requestedGridToWorld = (AffineTransform) gg.getGridToCRS2D();
return;
}
// //
//
// Use JAI ImageRead parameter
//
// //
if (name.equals(AbstractGridFormat.USE_JAI_IMAGEREAD.getName())) {
final Object value = param.getValue();
if (value == null)
return;
readType = param.booleanValue() ? ReadType.JAI_IMAGEREAD : ReadType.DIRECT_READ;
return;
}
// //
//
// Overview Policy parameter
//
// //
if (name.equals(AbstractGridFormat.OVERVIEW_POLICY.getName())) {
final Object value = param.getValue();
if (value == null)
return;
overviewPolicy = (OverviewPolicy) param.getValue();
return;
}
if (name.equals(ImageCollectionFormat.BACKGROUND_VALUES.getName())) {
final Object value = param.getValue();
if (value == null)
return;
backgroundValues = (double[]) value;
return;
}
if (name.equals(ImageCollectionFormat.FILTER.getName())) {
final Object value = param.getValue();
if (value == null)
return;
filter = (Filter) value;
return;
}
if (name.equals(ImageCollectionFormat.SUGGESTED_TILE_SIZE.getName())) {
final String suggestedTileSize = (String) param.getValue();
// Preliminary checks on parameter value
if ((suggestedTileSize != null) && (suggestedTileSize.trim().length() > 0)) {
if (suggestedTileSize.contains(ImageCollectionFormat.TILE_SIZE_SEPARATOR)) {
final String[] tilesSize = suggestedTileSize.split(ImageCollectionFormat.TILE_SIZE_SEPARATOR);
if (tilesSize.length == 2) {
try {
// Getting suggested tile size
final int tileWidth = Integer.valueOf(tilesSize[0].trim());
final int tileHeight = Integer.valueOf(tilesSize[1].trim());
tileDimensions = new Dimension(tileWidth, tileHeight);
} catch (NumberFormatException nfe) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "Unable to parse suggested tile size parameter");
}
}
}
}
}
}
}
/**
* Compute this specific request settings all the parameters needed by a
* visiting {@link RasterLayerResponse} object.
*
* @throws DataSourceException
*/
private void prepare() throws DataSourceException {
String path = null;
if (filter != null) {
path = parsePath(filter);
} else {
if (LOGGER.isLoggable(Level.INFO)){
LOGGER.info("No PATH have been specified through a Filter. Proceeding with default Image");
}
if (rasterManager.parent.defaultValues.path == null){
imageManager = rasterManager.getDatasetManager(Utils.FAKE_IMAGE_PATH);
return;
} else {
path = rasterManager.parent.defaultValues.path;
}
}
final String storePath = rasterManager.parent.rootPath;
//First normalization
path = FilenameUtils.normalize(path);
if (path.startsWith(storePath)){
// Removing the store path prefix from the specified path
// allow to deal with the case of parentPath = /home/user1/folder1/ and path = /home/user1/folder1/folder3
// which comes back to path = folder3
path = path.substring(storePath.length());
}
final String filePath = FilenameUtils.normalize(FilenameUtils.concat(storePath, path));
if (!filePath.startsWith(storePath)){
throw new DataSourceException("Possible attempt to access data outside the coverate store path:\n"
+ "Store Path: " + storePath + "\nSpecified File Path: " + filePath);
}
imageManager = rasterManager.getDatasetManager(filePath);
//
// DO WE HAVE A REQUESTED AREA?
//
// Check if we have something to load by intersecting the
// requested envelope with the bounds of this data set.
//
if (requestedBBox == null) {
//
// In case we have nothing to look at we should get the whole
// coverage
//
requestedBBox = imageManager.coverageBBox;
cropBBox = imageManager.coverageBBox;
requestedRasterArea = (Rectangle) imageManager.coverageRasterArea.clone();
destinationRasterArea = (Rectangle) imageManager.coverageRasterArea.clone();
requestedResolution = imageManager.coverageFullResolution.clone();
// TODO harmonize the various types of transformations
requestedGridToWorld = (AffineTransform) imageManager.coverageGridToWorld2D;
return;
}
//
// Adjust requested bounding box and source region in order to fall
// within the source coverage
//
computeRequestSpatialElements();
}
/**
* Extract a PATH from the specified filter. Filter should be an IsEqualsToImpl in order to support
* Filtering like CQL_FILTER=PATH='folder2/subfolder1/sample.tif'
* @param filter
* @return
*/
private String parsePath(Filter filter) {
if (filter == null){
throw new IllegalArgumentException("The provided filter is null");
}
//Workaround to deal with the case of Filter.INCLUDE
//being saved when creating the store
if (filter instanceof IncludeFilter){
if (LOGGER.isLoggable(Level.FINE)){
LOGGER.fine("Using DEFAULT Path");
}
return rasterManager.parent.defaultValues.path;
}
if (!(filter instanceof IsEqualsToImpl)){
throw new IllegalArgumentException("The provided filter should be an \"equals to\" filter: \"" + PATH_KEY + "=value\"");
}
IsEqualsToImpl pathEqualTo = (IsEqualsToImpl) filter;
AttributeExpressionImpl pathKey = (AttributeExpressionImpl) pathEqualTo.getExpression1();
String pathK = (String) pathKey.getPropertyName();
if (!pathK.equalsIgnoreCase(PATH_KEY)){
throw new IllegalArgumentException("Invalid filter specified. It should be like this: \"" + PATH_KEY + "=value\" whilst the first expression is " + pathK);
}
Literal pathValue = (Literal) pathEqualTo.getExpression2();
String pathV = (String) pathValue.getValue();
return pathV;
}
/**
* Check the type of read operation which will be performed and return
* {@code true} if a JAI imageRead operation need to be performed or
* {@code false} if a simple read operation is needed.
*
* @return {@code true} if the read operation will use a JAI ImageRead
* operation instead of a simple {@code ImageReader.read(...)} call.
*/
private void checkReadType() {
if (readType != ReadType.UNSPECIFIED)
return;
// //
//
// Ok, the request did not explicitly set the read type, let's check the
// hints.
//
// //
final Hints hints = rasterManager.getHints();
if (hints != null) {
final Object o = hints.get(Hints.USE_JAI_IMAGEREAD);
if (o != null) {
readType = (ReadType) o;
return;
}
}
// //
//
// Last chance is to use the default read type.
//
// //
readType = ReadType.getDefault();
}
/**
* Return a crop region from a specified envelope, leveraging on the grid to
* world transformation.
*
* @param refinedRequestedBBox
* the crop envelope
* @return a {@code Rectangle} representing the crop region.
* @throws TransformException
* in case a problem occurs when going back to raster space.
* @throws DataSourceException
*/
private void computeCropRasterArea() throws DataSourceException {
// we have nothing to crop
if (cropBBox == null) {
destinationRasterArea = null;
return;
}
//
// We need to invert the requested gridToWorld and then adjust the
// requested raster area are accordingly
//
// invert the requested grid to world keeping into account the fact that
// it is related to cell center
// while the raster is related to cell corner
MathTransform2D requestedWorldToGrid;
try {
requestedWorldToGrid = (MathTransform2D) PixelTranslation.translate(
ProjectiveTransform.create(requestedGridToWorld),
PixelInCell.CELL_CENTER, PixelInCell.CELL_CORNER)
.inverse();
} catch (NoninvertibleTransformException e) {
throw new DataSourceException(e);
}
// now get the requested bbox which have been already adjusted and
// project it back to raster space
try {
destinationRasterArea = new GeneralGridEnvelope(CRS.transform(
requestedWorldToGrid, new GeneralEnvelope(cropBBox)),
PixelInCell.CELL_CORNER, false).toRectangle();
} catch (IllegalStateException e) {
throw new DataSourceException(e);
} catch (TransformException e) {
throw new DataSourceException(e);
}
// is it empty??
if (destinationRasterArea.isEmpty()) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE,
"Requested envelope too small resulting in empty cropped raster region");
// TODO: Future versions may define a 1x1 rectangle starting
// from the lower coordinate
empty = true;
return;
}
}
/**
* @throws DataSourceException
* in case something bad occurs
*/
private void computeRequestSpatialElements() throws DataSourceException {
// Create the crop bbox in the coverage CRS for cropping it later on.
computeCropBBOX();
if (empty || (cropBBox != null && cropBBox.isEmpty())) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "RequestedBBox empty or null");
// this means that we do not have anything to load at all!
empty = true;
return;
}
//
// CROP SOURCE REGION using the refined requested envelope
//
computeCropRasterArea();
if (empty || (destinationRasterArea != null && destinationRasterArea.isEmpty())) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "CropRasterArea empty or null");
// this means that we do not have anything to load at all!
return;
}
if (LOGGER.isLoggable(Level.FINE)) {
StringBuffer sb = new StringBuffer("Adjusted Requested Envelope = ")
.append(requestedBBox.toString()).append("\n")
.append("Requested raster dimension = ")
.append(requestedRasterArea.toString()).append("\n")
.append("Corresponding raster source region = ")
.append(requestedRasterArea.toString());
LOGGER.log(Level.FINE, sb.toString());
}
//
// Compute the request resolution from the request
//
computeRequestedResolution();
}
/**
* Computes the requested resolution which is going to be used for selecting
* overviews and or deciding decimation factors on the target coverage.
*
* <p>
* In case the requested envelope is in the same
* {@link CoordinateReferenceSystem} of the coverage we compute the
* resolution using the requested {@link MathTransform}. Notice that it must
* be a {@link LinearTransform} or else we fail.
*
* <p>
* In case the requested envelope is not in the same
* {@link CoordinateReferenceSystem} of the coverage we
*
* @throws DataSourceException
* in case something bad happens during reprojections and/or
* intersections.
*/
private void computeRequestedResolution() throws DataSourceException {
try {
// let's try to get the resolution from the requested gridToWorld
if (requestedGridToWorld instanceof LinearTransform) {
//
// the crs of the request and the one of the coverage are
// the
// same, we can get the resolution from the grid to world
//
requestedResolution = new double[] {
XAffineTransform.getScaleX0(requestedGridToWorld),
XAffineTransform.getScaleY0(requestedGridToWorld) };
}
// leave
return;
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.INFO))
LOGGER.log(Level.INFO, "Unable to compute requested resolution", e);
}
//
// use the coverage resolution since we cannot compute the requested one
//
LOGGER.log(Level.WARNING, "Unable to compute requested resolution, using highest available");
requestedResolution = imageManager.coverageFullResolution;
}
private void computeCropBBOX() throws DataSourceException {
// we do not need to do anything, but we do this in order to
// avoid problems with the envelope checks
cropBBox = new ReferencedEnvelope(requestedBBox.getMinX(),
requestedBBox.getMaxX(), requestedBBox.getMinY(),
requestedBBox.getMaxY(),
imageManager.coverageCRS);
//
// intersect requested BBox in with coverage bbox to get the crop bbox
//
if (!cropBBox.intersects((BoundingBox) imageManager.coverageBBox)) {
cropBBox = null;
empty = true;
return;
}
// TODO XXX Optimize when referenced envelope has intersection
// method that actually retains the CRS, this is the JTS one
cropBBox = new ReferencedEnvelope(((ReferencedEnvelope) cropBBox).intersection(imageManager.coverageBBox),
imageManager.coverageCRS);
return;
}
public boolean isEmpty() {
return empty;
}
public BoundingBox getRequestedBBox() {
return requestedBBox;
}
public double[] getBackgroundValues() {
return backgroundValues;
}
public OverviewPolicy getOverviewPolicy() {
return overviewPolicy;
}
public Rectangle getRequestedRasterArea() {
return (Rectangle) (requestedRasterArea != null ? requestedRasterArea
.clone() : requestedRasterArea);
}
public double[] getRequestedResolution() {
return requestedResolution != null ? requestedResolution.clone() : null;
}
public ReadType getReadType() {
return readType;
}
public Rectangle getDestinationRasterArea() {
return destinationRasterArea;
}
public BoundingBox getCropBBox() {
return cropBBox;
}
public AffineTransform getRequestedGridToWorld() {
return requestedGridToWorld;
}
public Dimension getTileDimensions() {
return tileDimensions;
}
public Filter getFilter() {
return filter;
}
public double[] getRequestedRasterScaleFactors() {
return requestedRasterScaleFactors != null ? requestedRasterScaleFactors
.clone() : requestedRasterScaleFactors;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("RasterLayerRequest description: \n");
builder.append("\tRequestedBBox=").append(requestedBBox).append("\n");
builder.append("\tRequestedRasterArea=").append(requestedRasterArea)
.append("\n");
builder.append("\tRequestedGridToWorld=").append(requestedGridToWorld)
.append("\n");
builder.append("\tReadType=").append(readType);
return builder.toString();
}
}