org.geotools.gce.imagecollection.RasterLayerRequest.java Source code

Java tutorial

Introduction

Here is the source code for org.geotools.gce.imagecollection.RasterLayerRequest.java

Source

/*
 *    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();
    }
}