/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2015, 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.imagemosaic;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.coverage.grid.GeneralGridEnvelope;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.ReadResolutionCalculator;
import org.geotools.data.DataSourceException;
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.builder.GridToEnvelopeMapper;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.geometry.XRectangle2D;
import org.geotools.util.Utilities;
import org.opengis.geometry.BoundingBox;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.FactoryException;
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;
/**
* Helper class which takes coverage's spatial information input (CRS, bbox, resolution,...) and a set of request's parameters (requestedCRS,
* requestedBBox, requested resolution, ...) and takes care of computing all auxiliary spatial variables for future computations.
*
*
* @author Daniele Romagnoli, GeoSolutions SAS
*
*/
public class SpatialRequestHelper {
public SpatialRequestHelper(CoverageProperties coverageProperties) {
super();
this.coverageProperties = coverageProperties;
}
public static class CoverageProperties {
public ReferencedEnvelope getBbox() {
return bbox;
}
public void setBBox(ReferencedEnvelope bbox) {
this.bbox = bbox;
}
public Rectangle getRasterArea() {
return rasterArea;
}
public void setRasterArea(Rectangle rasterArea) {
this.rasterArea = rasterArea;
}
public double[] getFullResolution() {
return fullResolution;
}
public void setFullResolution(double[] fullResolution) {
this.fullResolution = fullResolution;
}
public MathTransform2D getGridToWorld2D() {
return gridToWorld2D;
}
public void setGridToWorld2D(MathTransform2D gridToWorld2D) {
this.gridToWorld2D = gridToWorld2D;
}
public CoordinateReferenceSystem getCrs2D() {
return crs2D;
}
public void setCrs2D(CoordinateReferenceSystem crs2d) {
crs2D = crs2d;
}
public ReferencedEnvelope getGeographicBBox() {
return geographicBBox;
}
public void setGeographicBBox(ReferencedEnvelope geographicBBox) {
this.geographicBBox = geographicBBox;
}
public CoordinateReferenceSystem getGeographicCRS2D() {
return geographicCRS;
}
public void setGeographicCRS2D(CoordinateReferenceSystem geographicCRS2D) {
this.geographicCRS = geographicCRS2D;
}
// //
// Source Coverages properties
// //
ReferencedEnvelope bbox;
Rectangle rasterArea;
double[] fullResolution;
MathTransform2D gridToWorld2D;
CoordinateReferenceSystem crs2D;
ReferencedEnvelope geographicBBox;
CoordinateReferenceSystem geographicCRS;
}
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(SpatialRequestHelper.class);
/** The {@link BoundingBox} requested */
private ReferencedEnvelope requestedBBox;
/** The {@link BoundingBox} of the portion of the coverage that intersects the requested bbox */
private ReferencedEnvelope computedBBox;
/** The region where to fit the requested envelope */
private Rectangle requestedRasterArea;
/** The region of the */
private Rectangle computedRasterArea;
private CoordinateReferenceSystem requestCRS;
private AffineTransform requestedGridToWorld;
private double[] computedResolution;
private GeneralEnvelope requestedBBOXInCoverageGeographicCRS;
private MathTransform requestCRSToCoverageGeographicCRS2D;
private MathTransform destinationToSourceTransform;
private final CoverageProperties coverageProperties;
private boolean accurateResolution;
/**
* Set to {@code true} if this request will produce an empty result, and the coverageResponse will produce a {@code null} coverage.
*/
private boolean emptyRequest;
private boolean needsReprojection = false;
private GeneralEnvelope approximateRequestedBBoInNativeCRS;
/** The final Grid To World. In case there is a reprojection involved it is not the original one. */
private AffineTransform computedGridToWorld;
private GridGeometry2D requestedGridGeometry;
public void setRequestedGridGeometry(GridGeometry2D gridGeometry) {
Utilities.ensureNonNull("girdGeometry", gridGeometry);
requestedBBox = new ReferencedEnvelope((Envelope) gridGeometry.getEnvelope2D());
requestedRasterArea = gridGeometry.getGridRange2D().getBounds();
requestedGridGeometry = gridGeometry;
requestedGridToWorld = (AffineTransform) gridGeometry.getGridToCRS2D();
}
/**
* Compute this specific request settings all the parameters needed by a visiting {@link RasterLayerResponse} object.
*
* @throws DataSourceException
*/
public void compute() throws DataSourceException {
//
// 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 = new ReferencedEnvelope(coverageProperties.bbox,
coverageProperties.crs2D);
requestedRasterArea = (Rectangle) coverageProperties.rasterArea.clone();
computedResolution = coverageProperties.fullResolution.clone();
computedBBox = new ReferencedEnvelope(coverageProperties.bbox,
coverageProperties.crs2D);
computedRasterArea = (Rectangle) coverageProperties.rasterArea.clone();
// TODO harmonize the various types of transformations
computedGridToWorld = requestedGridToWorld = (AffineTransform) coverageProperties.gridToWorld2D;
// account for an empty coverage --> set request empty
if (requestedBBox.isEmpty())
emptyRequest = true;
return;
}
//
// WE DO HAVE A REQUESTED AREA!
//
//
// Inspect the request and precompute transformation between CRS. We
// also check if we can simply adjust the requested GG in case the
// request CRS is different from the coverage native CRS but the
// transformation is simply an affine transformation.
//
// In such a case we can simplify our work by adjusting the
// requested grid to world, preconcatenating the coordinate
// operation to change CRS
//
inspectCoordinateReferenceSystems();
//
// Create the CROP BBOX in the coverage CRS for cropping it later on.
//
computeCropBBOX();
if (emptyRequest || computedBBox == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "RequestedBBox empty or null");
}
return;
}
//
// CROP SOURCE REGION using the refined requested envelope
//
computeRasterArea();
if (emptyRequest || computedRasterArea == null) {
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.FINER)) {
StringBuilder sb = new StringBuilder("Adjusted Requested Envelope = ")
.append(requestedBBox.toString()).append("\n")
.append("Requested raster dimension = ").append(requestedRasterArea.toString())
.append("\n").append("Corresponding raster source region = ")
.append(computedRasterArea.toString()).append("\n")
.append("Corresponding source Envelope = ").append(computedBBox.toString());
LOGGER.log(Level.FINER, sb.toString());
}
//
// Compute the request resolution from the request
//
computeResolution();
}
private void inspectCoordinateReferenceSystems() throws DataSourceException {
// get the crs for the requested bbox
requestCRS = CRS.getHorizontalCRS(requestedBBox.getCoordinateReferenceSystem());
//
// Check if the request CRS is different from the coverage native CRS
//
if (!CRS.equalsIgnoreMetadata(requestCRS, coverageProperties.crs2D))
try {
destinationToSourceTransform = CRS.findMathTransform(requestCRS,
coverageProperties.crs2D, true);
} catch (FactoryException e) {
throw new DataSourceException("Unable to inspect request CRS", e);
}
// now transform the requested envelope to source crs
if (destinationToSourceTransform != null) {
if (destinationToSourceTransform.isIdentity()) {
// the CRS is basically the same
destinationToSourceTransform = null;
// we need to use the coverage one for the requested bbox to avoid problems later on
this.requestedBBox = new ReferencedEnvelope(requestedBBox,
coverageProperties.crs2D);
} else {
// we do need to reproject
needsReprojection = true;
//
// k, the transformation between the various CRS is not null or the
// Identity, let's see if it is an affine transform, which case we
// can incorporate it into the requested grid to world
if (destinationToSourceTransform instanceof AffineTransform) {
//
// we should not have any problems with regards to BBOX reprojection
// update the requested grid to world transformation by pre concatenating the
// destination to source transform
AffineTransform mutableTransform = (AffineTransform) requestedGridToWorld
.clone();
mutableTransform.preConcatenate((AffineTransform) destinationToSourceTransform);
// update the requested envelope
try {
final MathTransform tempTransform = PixelTranslation.translate(
ProjectiveTransform.create(mutableTransform),
PixelInCell.CELL_CENTER, PixelInCell.CELL_CORNER);
requestedBBox = new ReferencedEnvelope(CRS.transform(tempTransform,
new GeneralEnvelope(requestedRasterArea)));
} catch (Exception e) {
throw new DataSourceException("Unable to inspect request CRS", e);
}
// now clean up all the traces of the transformations
destinationToSourceTransform = null;
needsReprojection = false;
}
}
}
}
/**
* 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 computeRasterArea() throws DataSourceException {
// we have nothing to crop
if (emptyRequest || computedBBox == null) {
throw new IllegalStateException(
"IllegalState, unable to compute raster area for null bbox");
}
try {
//
// 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 = (MathTransform2D) PixelTranslation
.translate(ProjectiveTransform.create(requestedGridToWorld),
PixelInCell.CELL_CENTER, PixelInCell.CELL_CORNER)
.inverse();
if (!needsReprojection) {
// now get the requested bbox which have been already adjusted and project it back
// to raster space
computedRasterArea = new GeneralGridEnvelope(
CRS.transform(requestedWorldToGrid, new GeneralEnvelope(computedBBox)),
PixelInCell.CELL_CORNER, false).toRectangle();
} else {
//
// reproject the crop bbox back in the requested crs and then crop, notice that we
// are imposing
// the same raster area somehow
//
final GeneralEnvelope cropBBOXInRequestCRS = CRS.transform(computedBBox,
requestCRS);
// make sure it falls within the requested envelope
cropBBOXInRequestCRS.intersect(requestedBBox);
// now go back to raster space
computedRasterArea = new GeneralGridEnvelope(
CRS.transform(requestedWorldToGrid, cropBBOXInRequestCRS),
PixelInCell.CELL_CORNER, false).toRectangle();
// intersect with the original requested raster space to be sure that we stay within
// the requested raster area
XRectangle2D.intersect(computedRasterArea, requestedRasterArea, computedRasterArea);
}
} catch (Exception e) {
throw new DataSourceException(e);
}
// is it empty??
if (computedRasterArea.isEmpty()) {
// TODO: Future versions may define a 1x1 rectangle starting
// from the lower coordinate
emptyRequest = true;
return;
}
}
/**
* 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 do an in place reprojection.
*
*/
private void computeResolution() {
try {
//
// the crs of the request and the one of the coverage are NOT the
// same and the conversion is not , we can get the resolution from envelope + raster directly
//
GridGeometry2D gridGeometry;
if (needsReprojection) {
final GridToEnvelopeMapper geMapper = new GridToEnvelopeMapper(
new GridEnvelope2D(computedRasterArea), computedBBox);
computedGridToWorld = geMapper.createAffineTransform();
if (accurateResolution) {
gridGeometry = requestedGridGeometry;
} else {
gridGeometry = new GridGeometry2D(new GridEnvelope2D(computedRasterArea),
computedBBox);
}
} else {
gridGeometry = requestedGridGeometry;
computedGridToWorld = requestedGridToWorld;
}
ReadResolutionCalculator calculator = new ReadResolutionCalculator(gridGeometry,
coverageProperties.crs2D, coverageProperties.fullResolution);
calculator.setAccurateResolution(accurateResolution);
computedResolution = calculator
.computeRequestedResolution(ReferencedEnvelope.reference(computedBBox));
// 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, this can be problematic but at least keep us going
//
LOGGER.log(Level.WARNING,
"Unable to compute requested resolution, using highest available");
computedResolution = coverageProperties.fullResolution;
}
/**
* Classic way of computing the requested resolution
*
* @return
*/
private double[] computeClassicResolution() {
return new double[] { XAffineTransform.getScaleX0(computedGridToWorld),
XAffineTransform.getScaleY0(computedGridToWorld) };
}
/**
* Compute the resolutions through a more accurate logic: Compute the resolution in 9 points, the corners of the requested area and the middle
* points and take the best one. This will provide better results for cases where there is a lot more deformation on a subregion
* (top/bottom/sides) of the requested bbox with respect to others.
*
* @return
* @throws TransformException
* @throws NoninvertibleTransformException
*/
private double[] computeAccurateResolution()
throws TransformException, NoninvertibleTransformException {
// transform back to the original space the brop bbox
GeneralEnvelope cropBboxTarget = CRS.transform(computedBBox, requestCRS);
// get the requested resolution
double requestedResolution[] = new double[] {
XAffineTransform.getScaleX0(requestedGridToWorld),
XAffineTransform.getScaleY0(requestedGridToWorld), };
double[] points = new double[36];
for (int i = 0; i < 3; i++) {
double x;
if (i == 0) {
x = cropBboxTarget.getMinimum(0);
} else if (i == 1) {
x = cropBboxTarget.getMedian(0);
} else {
x = cropBboxTarget.getMaximum(0);
}
for (int j = 0; j < 3; j++) {
double y;
if (j == 0) {
y = cropBboxTarget.getMinimum(1);
} else if (j == 1) {
y = cropBboxTarget.getMedian(1);
} else {
y = cropBboxTarget.getMaximum(1);
}
int k = (i * 3 + j) * 4;
points[k] = x - requestedResolution[0] / 2;
points[k + 1] = y - requestedResolution[1] / 2;
points[k + 2] = x + requestedResolution[0] / 2;
points[k + 3] = y + requestedResolution[1] / 2;
}
}
destinationToSourceTransform.transform(points, 0, points, 0, 18);
double mx = Double.MAX_VALUE;
double my = Double.MAX_VALUE;
for (int i = 0; i < 36; i += 4) {
double dx = points[i + 2] - points[i];
double dy = points[i + 3] - points[i + 1];
if (dx < mx) {
mx = dx;
}
if (dy < my) {
my = dy;
}
}
return new double[] { mx, my };
}
private void computeCropBBOX() throws DataSourceException {
try {
//
// The destination to source transform has been computed (and eventually erased) already
// by inspectCoordinateSystem()
// now transform the requested envelope to source crs
if (needsReprojection) {
final GeneralEnvelope requestedBBoxInCoverageCRS = CRS.transform(requestedBBox,
coverageProperties.crs2D);
computedBBox = new ReferencedEnvelope(requestedBBoxInCoverageCRS);
} else {
// we do not need to do anything, but we do this in order to avoid problems with the envelope checks
computedBBox = new ReferencedEnvelope(requestedBBox);
}
// intersect requested BBox in native CRS with coverage native bbox to get the crop bbox
// intersect the requested area with the bounds of this layer in native crs
if (!computedBBox.intersects((BoundingBox) coverageProperties.bbox)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(
new StringBuilder("The computed CropBoundingBox ").append(computedBBox)
.append(" Doesn't intersect the coverage BoundingBox ")
.append(coverageProperties.bbox)
.append(" resulting in an empty request").toString());
}
computedBBox = null;
emptyRequest = true;
return;
}
// TODO XXX Optimize when referenced envelope has intersection method that actually retains the CRS, this is the JTS one
computedBBox = new ReferencedEnvelope(
computedBBox.intersection(coverageProperties.bbox), coverageProperties.crs2D);
if (computedBBox.isEmpty()) {
// this means that we do not have anything to load at all!
emptyRequest = true;
}
return;
} catch (TransformException te) {
// something bad happened while trying to transform this
// envelope. let's try with wgs84
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, te.getLocalizedMessage(), te);
}
try {
// can we proceed? Do we have geo stuff to do all these operations?
if (coverageProperties.geographicCRS != null
&& coverageProperties.geographicBBox != null) {
//
// If we can not reproject the requested envelope to the native CRS,
// we go back to reproject in the geographic crs of the native
// coverage since this usually happens for conversions between CRS
// whose area of definition is different
//
// STEP 1 reproject the requested envelope to the coverage geographic bbox
if (!CRS.equalsIgnoreMetadata(coverageProperties.geographicCRS, requestCRS)) {
// try to convert the requested bbox to the coverage geocrs
requestedBBOXInCoverageGeographicCRS = CRS.transform(requestedBBox,
coverageProperties.geographicCRS);
requestedBBOXInCoverageGeographicCRS
.setCoordinateReferenceSystem(coverageProperties.geographicCRS);
}
if (requestedBBOXInCoverageGeographicCRS == null) {
requestedBBOXInCoverageGeographicCRS = new GeneralEnvelope(requestCRS);
}
// STEP 2 intersection with the geographic bbox for this coverage
if (!requestedBBOXInCoverageGeographicCRS
.intersects(coverageProperties.geographicBBox, true)) {
computedBBox = null;
emptyRequest = true;
return;
}
// intersect with the coverage native geographic bbox
// note that for the moment we got to use general envelope since there is no intersection otherwise
requestedBBOXInCoverageGeographicCRS.intersect(coverageProperties.geographicBBox);
requestedBBOXInCoverageGeographicCRS
.setCoordinateReferenceSystem(coverageProperties.geographicCRS);
// now go back to the coverage native CRS in order to compute an approximate requested resolution
approximateRequestedBBoInNativeCRS = CRS
.transform(requestedBBOXInCoverageGeographicCRS, coverageProperties.crs2D);
approximateRequestedBBoInNativeCRS
.setCoordinateReferenceSystem(coverageProperties.crs2D);
computedBBox = new ReferencedEnvelope(approximateRequestedBBoInNativeCRS);
return;
}
} catch (Exception e) {
// something bad happened while trying to transform this
// envelope. let's try with wgs84
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, e.getLocalizedMessage(), e);
}
LOGGER.log(Level.INFO,
"We did not manage to crop the requested envelope, we fall back onto loading the whole coverage.");
computedBBox = null;
}
public boolean isEmpty() {
return emptyRequest;
}
public boolean isNeedsReprojection() {
return needsReprojection;
}
public boolean isAccurateResolution() {
return accurateResolution;
}
public void setAccurateResolution(boolean accurateResolution) {
this.accurateResolution = accurateResolution;
}
public double[] getComputedResolution() {
return computedResolution != null ? computedResolution.clone() : null;
}
public Rectangle getComputedRasterArea() {
return (Rectangle) (computedRasterArea != null ? computedRasterArea.clone()
: computedRasterArea);
}
public BoundingBox getComputedBBox() {
return computedBBox;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("SpatialRequestHelper [");
if (requestedBBox != null) {
builder.append("requestedBBox=");
builder.append(requestedBBox);
builder.append(", ");
}
if (computedBBox != null) {
builder.append("cropBBox=");
builder.append(computedBBox);
builder.append(", ");
}
if (requestedRasterArea != null) {
builder.append("requestedRasterArea=");
builder.append(requestedRasterArea);
builder.append(", ");
}
if (computedRasterArea != null) {
builder.append("destinationRasterArea=");
builder.append(computedRasterArea);
builder.append(", ");
}
if (requestCRS != null) {
builder.append("requestCRS=");
builder.append(requestCRS);
builder.append(", ");
}
if (requestedGridToWorld != null) {
builder.append("requestedGridToWorld=");
builder.append(requestedGridToWorld);
builder.append(", ");
}
if (computedResolution != null) {
builder.append("requestedResolution=");
builder.append(Arrays.toString(computedResolution));
builder.append(", ");
}
if (requestedBBOXInCoverageGeographicCRS != null) {
builder.append("requestedBBOXInCoverageGeographicCRS=");
builder.append(requestedBBOXInCoverageGeographicCRS);
builder.append(", ");
}
if (requestCRSToCoverageGeographicCRS2D != null) {
builder.append("requestCRSToCoverageGeographicCRS2D=");
builder.append(requestCRSToCoverageGeographicCRS2D);
builder.append(", ");
}
if (destinationToSourceTransform != null) {
builder.append("destinationToSourceTransform=");
builder.append(destinationToSourceTransform);
builder.append(", ");
}
if (coverageProperties != null) {
builder.append("coverageProperties=");
builder.append(coverageProperties);
builder.append(", ");
}
builder.append("accurateResolution=");
builder.append(accurateResolution);
builder.append(", empty=");
builder.append(emptyRequest);
builder.append(", needsReprojection=");
builder.append(needsReprojection);
builder.append(", ");
if (approximateRequestedBBoInNativeCRS != null) {
builder.append("approximateRequestedBBoInNativeCRS=");
builder.append(approximateRequestedBBoInNativeCRS);
}
builder.append("]");
return builder.toString();
}
public AffineTransform getComputedGridToWorld() {
return computedGridToWorld;
}
}