This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 54fd75a Refactor WraparoundAdjustment in a way that makes easier to
share code between the method working on Envelope and the method working on
DirectPosition.
54fd75a is described below
commit 54fd75a91ee549e9474e8431850262122afb3ac5
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Apr 7 21:05:56 2019 +0200
Refactor WraparoundAdjustment in a way that makes easier to share code
between the method working on Envelope and the method working on DirectPosition.
---
.../apache/sis/coverage/grid/GridDerivation.java | 69 +--
.../sis/coverage/grid/GridDerivationTest.java | 73 ++-
.../internal/referencing/WraparoundAdjustment.java | 568 +++++++++++++--------
.../referencing/WraparoundAdjustmentTest.java | 15 +-
4 files changed, 430 insertions(+), 295 deletions(-)
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index a0172e5..1c7ef9c 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -76,6 +76,7 @@ import org.opengis.coverage.PointOutsideCoverageException;
* For dimensionality reduction, see {@link GridGeometry#reduce(int...)}.</p>
*
* @author Martin Desruisseaux (Geomatys)
+ * @author Alexis Manin (Geomatys)
* @version 1.0
*
* @see GridGeometry#derive()
@@ -490,7 +491,7 @@ public class GridDerivation {
cornerToCRS = MathTransforms.concatenate(cornerToCRS,
baseToAOI.getMathTransform());
}
/*
- * If the envelope dimensions does not encompass all grid
dimensions, the envelope is probably non-invertible.
+ * If the envelope dimensions do not encompass all grid
dimensions, the transform is probably non-invertible.
* We need to reduce the number of grid dimensions in the
transform for having a one-to-one relationship.
*/
int dimension = cornerToCRS.getTargetDimensions();
@@ -504,9 +505,8 @@ public class GridDerivation {
dimension = baseExtent.getDimension(); // Non-null since
'base.requireGridToCRS()' succeed.
GeneralEnvelope indices = null;
if (areaOfInterest != null) {
- indices = new WraparoundAdjustment(areaOfInterest)
- .shiftInto(base.envelope, (baseToAOI != null) ?
baseToAOI.getMathTransform() : null)
- .result(cornerToCRS.inverse());
+ final MathTransform domainToAOI = (baseToAOI != null) ?
baseToAOI.getMathTransform() : null;
+ indices = new WraparoundAdjustment(base.envelope, domainToAOI,
cornerToCRS.inverse()).shift(areaOfInterest);
clipExtent(indices);
}
if (indices == null || indices.getDimension() != dimension) {
@@ -721,15 +721,17 @@ public class GridDerivation {
* before to invoke this method.</li>
* <li>This method does not reduce the number of dimensions of the grid
geometry.
* For dimensionality reduction, see {@link
GridGeometry#reduce(int...)}.</li>
+ * <li>If the given point is known to be expressed in the same CRS than
the grid geometry,
+ * then the {@linkplain
DirectPosition#getCoordinateReferenceSystem() CRS of the point}
+ * can be left unspecified ({@code null}). It may give a slight
performance improvement
+ * by avoiding the check for coordinate transformation.</li>
* </ul>
*
* @param slicePoint the coordinates where to get a slice. If no
coordinate reference system is attached to it,
* we consider it's the same as base grid geometry.
* @return {@code this} for method call chaining.
- * @throws IncompleteGridGeometryException if the base grid geometry has
no extent, no "grid to CRS" transform, or
- * if its coordinate reference system is unknown while input point one is
(in which case we're unable to find a
- * transform between base CRS and input point CRS). Note that if you're
sure that your point is expressed in base
- * CRS, you can give a point without any CRS, to avoid this check.
+ * @throws IncompleteGridGeometryException if the base grid geometry has
no extent, no "grid to CRS" transform,
+ * or no CRS (unless {@code slicePoint} has no CRS neither, in
which case the CRS are assumed the same).
* @throws IllegalGridGeometryException if an error occurred while
converting the point coordinates to grid coordinates.
* @throws PointOutsideCoverageException if the given point is outside the
grid extent.
*/
@@ -741,40 +743,47 @@ public class GridDerivation {
if (toBase != null) {
gridToCRS = MathTransforms.concatenate(toBase, gridToCRS);
}
-
- /* We'll try to find a link between base coordinate system and
input point one. Note that we allow unknown
- * CRS on slice point, in which case we consider it to be
expressed in base geometry system. However, if the
- * point CRS is known, but base geometry one is not, too much
ambiguity resides, and we'll throw an error.
+ /*
+ * We will try to find a path between grid coordinate reference
system (CRS) and given point CRS. Note that we
+ * allow unknown CRS on the slice point, in which case we consider
it to be expressed in grid reference system.
+ * However, if the point CRS is specified while the base grid CRS
is unknown, we are at risk of ambiguity,
+ * in which case we throw (indirectly) an
IncompleteGridGeometryException.
*/
- final MathTransform baseToSlice;
- final CoordinateReferenceSystem targetCRS =
slicePoint.getCoordinateReferenceSystem();
- if (targetCRS != null) {
- final CoordinateReferenceSystem sourceCRS =
base.getCoordinateReferenceSystem();
- baseToSlice = CRS.findOperation(sourceCRS, targetCRS, null)
- .getMathTransform();
- gridToCRS = MathTransforms.concatenate(gridToCRS, baseToSlice);
+ final CoordinateReferenceSystem sliceCRS =
slicePoint.getCoordinateReferenceSystem();
+ final MathTransform baseToPOI;
+ if (sliceCRS == null) {
+ baseToPOI = null;
} else {
- baseToSlice = null;
+ final CoordinateReferenceSystem gridCRS =
base.getCoordinateReferenceSystem(); // May throw exception.
+ baseToPOI = CRS.findOperation(gridCRS, sliceCRS,
null).getMathTransform();
+ gridToCRS = MathTransforms.concatenate(gridToCRS, baseToPOI);
}
-
+ /*
+ * If the point dimensions do not encompass all grid dimensions,
the transform is probably non-invertible.
+ * We need to reduce the number of grid dimensions in the
transform for having a one-to-one relationship.
+ */
final int dimension = gridToCRS.getTargetDimensions();
ArgumentChecks.ensureDimensionMatches("slicePoint", dimension,
slicePoint);
gridToCRS = dropUnusedDimensions(gridToCRS, dimension);
-
- GeneralEnvelope sliceEnvelope = new GeneralEnvelope(slicePoint,
slicePoint);
- sliceEnvelope = new WraparoundAdjustment(sliceEnvelope)
- .shiftInto(base.envelope, baseToSlice)
- .result(gridToCRS.inverse());
- DirectPosition gridPoint = sliceEnvelope.getMedian();
+ /*
+ * Take in account the case where the point could be inside the
grid if we apply a ±360° longitude shift.
+ * This is the same adjustment than for `subgrid(Envelope)`, but
applied on a DirectPosition. Calculation
+ * is done in units of cells of the GridGeometry to be created by
GridDerivation.
+ */
+ DirectPosition gridPoint = new WraparoundAdjustment(base.envelope,
baseToPOI, gridToCRS.inverse()).shift(slicePoint);
if (scaledExtent != null) {
scaledExtent = scaledExtent.slice(gridPoint,
modifiedDimensions);
}
-
- // Rebuild the extent without scaling
+ /*
+ * Above `scaledExtent` is non-null only if a scale or subsampling
has been applied before this `slice`
+ * method has been invoked. The `baseExtent` below contains same
information, but without subsampling.
+ * The subsampling effect is removed by applying the "scaled to
base" transform. Accuracy matter less
+ * here than for `scaledExtent` since the extent to be returned to
user is the later.
+ */
if (toBase != null) {
gridPoint = toBase.transform(gridPoint, gridPoint);
}
- baseExtent = baseExtent.slice(gridPoint, modifiedDimensions); //
Non-null check by 'base.requireGridToCRS()'.
+ baseExtent = baseExtent.slice(gridPoint, modifiedDimensions);
// Non-null check by 'base.requireGridToCRS()'.
} catch (FactoryException e) {
throw new
IllegalGridGeometryException(Resources.format(Resources.Keys.CanNotMapToGridDimensions),
e);
} catch (TransformException e) {
diff --git
a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
index 9c57c9d..58a197a 100644
---
a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
+++
b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
@@ -16,18 +16,19 @@
*/
package org.apache.sis.coverage.grid;
-import org.apache.sis.geometry.Envelope2D;
-import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
-import org.apache.sis.referencing.CommonCRS;
-import org.apache.sis.referencing.crs.DefaultCompoundCRS;
-import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.junit.Assert;
+import java.util.Collections;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.spatial.DimensionNameType;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.referencing.crs.DefaultCompoundCRS;
+import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.Matrix3;
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -40,10 +41,6 @@ import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestCase;
import org.junit.Test;
-import java.util.Collections;
-import java.util.stream.DoubleStream;
-import java.util.stream.IntStream;
-
import static org.apache.sis.test.ReferencingAssert.*;
import static org.apache.sis.coverage.grid.GridGeometryTest.assertExtentEquals;
@@ -52,6 +49,7 @@ import static
org.apache.sis.coverage.grid.GridGeometryTest.assertExtentEquals;
* Tests the {@link GridDerivation} implementation.
*
* @author Martin Desruisseaux (Geomatys)
+ * @author Alexis Manin (Geomatys)
* @version 1.0
* @since 1.0
* @module
@@ -264,16 +262,15 @@ public final strictfp class GridDerivationTest extends
TestCase {
}
/**
- * Check that wrap-around is well applied when using {@link
GridDerivation#slice(DirectPosition)}.
+ * Checks that wraparound is well applied when using {@link
GridDerivation#slice(DirectPosition)}.
*/
@Test
- public void testSliceWrapAround() {
+ public void testSliceWithWrapAround() {
final GridGeometry base = new GridGeometry(
PixelInCell.CELL_CORNER,
new AffineTransform2D(-0.02, 0, 0, 0.1, 55, 172),
- new Envelope2D(CommonCRS.WGS84.geographic(), 42, 172, 13, 51),
- GridRoundingMode.NEAREST
- );
+ new Envelope2D(HardCodedCRS.WGS84_φλ, 42, 172, 13, 51),
+ GridRoundingMode.NEAREST);
final GridGeometry expectedResult = base.derive()
.slice(new DirectPosition2D(51, 187))
@@ -283,28 +280,27 @@ public final strictfp class GridDerivationTest extends
TestCase {
.slice(new DirectPosition2D(51, -173))
.build();
- Assert.assertEquals("Slice with wrap-around", expectedResult,
fromWrapAround);
- assertBetween(
- "Wrapped Y coordinate",
- base.envelope.getMinimum(1),
- base.envelope.getMaximum(1),
- fromWrapAround.envelope.getMedian(1)
- );
+ assertEquals("Slice with wrap-around", expectedResult, fromWrapAround);
+ assertBetween("Wrapped Y coordinate",
+ base.envelope.getMinimum(1),
+ base.envelope.getMaximum(1),
+ fromWrapAround.envelope.getMedian(1));
}
/**
- * Ensure that slicing on a corner point does not fail, but gives back a
geometry centered on a pixel corner.
- * @throws TransformException If we cannot build our test point.
+ * Ensures that slicing on a corner point does not fail, but gives back a
grid geometry centered on a pixel corner.
+ *
+ * @throws TransformException if we cannot build our test point.
*/
@Test
- public void testSliceCorner() throws TransformException {
+ public void testSliceOnCorner() throws TransformException {
GridGeometry base = grid(-132, 327, 986, 597, 2, 3);
-
- // First of all, we'll try by focusing on the last pixel.
+ /*
+ * First of all, we'll try by focusing on the last pixel.
+ */
final DirectPosition2D gridUpperCorner = new DirectPosition2D(
base.extent.getHigh(0),
- base.extent.getLow(1)
- );
+ base.extent.getLow(1));
final DirectPosition geoUpperCorner =
base.getGridToCRS(PixelInCell.CELL_CENTER)
.transform(gridUpperCorner, null);
@@ -315,27 +311,26 @@ public final strictfp class GridDerivationTest extends
TestCase {
long[] expectedGridPoint = {(long) gridUpperCorner.x, (long)
gridUpperCorner.y};
assertExtentEquals(expectedGridPoint, expectedGridPoint, slice.extent);
-
- /* We will now try to focus on a point near the envelope edge. Note
that slicing ensures to return a valid grid
- * for any point INSIDE the envelope, it's non-deterministic about
points perfectly aligned on the edge. So,
- * here we will test a point very near to the envelope edge, but still
into it.
+ /*
+ * We will now try to focus on a point near the envelope edge. Note
that slicing ensures to return a valid grid
+ * for any point INSIDE the envelope, it's non-determinist about
points perfectly aligned on the edge.
+ * So, here we will test a point very near to the envelope edge, but
still into it.
*/
final GeneralEnvelope grid3d = new GeneralEnvelope(3);
grid3d.setEnvelope(0, 0, 0, 1920, 1080, 4);
final DefaultCompoundCRS crs3d = new DefaultCompoundCRS(
Collections.singletonMap("name", "geo3d"),
- CommonCRS.defaultGeographic(),
- CommonCRS.Temporal.JULIAN.crs()
- );
+ HardCodedCRS.WGS84,
+ HardCodedCRS.TIME);
+
final GeneralEnvelope geo3d = new GeneralEnvelope(crs3d);
geo3d.setEnvelope(-180, -90, 1865.128, 180, 90, 1865.256);
base = new GridGeometry(
PixelInCell.CELL_CORNER,
MathTransforms.linear(Matrices.createTransform(grid3d, geo3d)),
geo3d,
- GridRoundingMode.NEAREST
- );
+ GridRoundingMode.NEAREST);
final GeneralDirectPosition geo3dUpperCorner = new
GeneralDirectPosition(geo3d.getUpperCorner());
IntStream.range(0, geo3dUpperCorner.getDimension())
@@ -348,7 +343,7 @@ public final strictfp class GridDerivationTest extends
TestCase {
// Build expected grid point focused after slicing. We expect it to be
upper corner.
expectedGridPoint =
DoubleStream.of(grid3d.getUpperCorner().getCoordinate())
.mapToLong(value -> (long) value)
- .map(exclusiveValue -> exclusiveValue -1)// Exclusive to
inclusive
+ .map(exclusiveValue -> exclusiveValue - 1) // Exclusive
to inclusive.
.toArray();
assertExtentEquals(expectedGridPoint, expectedGridPoint, slice.extent);
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
index fb66213..a18fa73 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
@@ -35,11 +35,13 @@ import org.apache.sis.measure.Units;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.GeneralDirectPosition;
/**
* Adjustments applied on an envelope for handling wraparound axes. The
adjustments consist in shifting
- * some axes by an integer amount of periods, typically (not necessarily) 360°
of longitude.
+ * coordinate values on some axes by an integer amount of periods (typically
360° of longitude) in order
+ * to move an envelope or a position inside a given domain of validity.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
@@ -48,25 +50,70 @@ import org.apache.sis.geometry.GeneralEnvelope;
*/
public final class WraparoundAdjustment {
/**
- * The envelope to potentially shift in order to fit in the domain of
validity. If a shift is needed, then
- * this envelope will be replaced by a new envelope; the user-specified
envelope will not be modified.
+ * The region inside which a given Area Of Interest (AOI) or Point Of
Interest (POI) should be located.
+ * This envelope may be initially in a projected CRS and converted later
to geographic CRS in order to
+ * allow identification of wraparound axes.
*/
- private Envelope areaOfInterest;
+ private Envelope domainOfValidity;
/**
- * If {@link #areaOfInterest} has been converted to a geographic CRS, the
transformation back to its original CRS.
- * Otherwise {@code null}.
+ * If the AOI or POI does not use the same CRS than {@link
#domainOfValidity}, the transformation from
+ * {@code domainOfValidity} to the AOI / POI. Otherwise {@code null}.
+ *
+ * <div class="note"><b>Note:</b>
+ * this class does not check by itself if a coordinate operation is
needed; it must be supplied. We do that
+ * because {@code WraparoundAdjustment} is currently used in contexts
where this transform is known anyway,
+ * so we avoid to compute it twice.</div>
+ */
+ private final MathTransform domainToAOI;
+
+ /**
+ * A transform from the {@link #domainOfValidity} CRS to any user space at
caller choice.
+ * The object returned by {@code shift} will be transformed by this
transform after all computations
+ * have been finished. This is done in order to allow final transforms to
be concatenated in a single step.
+ */
+ private final MathTransform domainToAny;
+
+ /**
+ * If {@code areaOfInterest} or {@code pointOfInterest} has been converted
to a geographic CRS,
+ * the transformation back to its original CRS. Otherwise {@code null}.
*/
private MathTransform geographicToAOI;
/**
- * Creates a new instance for adjusting the given envelope.
- * The given envelope will not be modified; a copy will be created if
needed.
+ * The coordinate reference system of the Area Of Interest (AOI) or Point
of Interest (POI).
+ * May be replaced by a geographic CRS if the AOI or POI originally used a
projected CRS.
+ */
+ private CoordinateReferenceSystem crs;
+
+ /**
+ * Whether {@link #domainOfValidity} has been transformed to the
geographic CRS that is the source
+ * of {@link #geographicToAOI}. This flag is used for ensuring that {@link
#replaceCRS()} performs
+ * the inverse projection only once.
+ */
+ private boolean isDomainTransformed;
+
+ /**
+ * Whether the Area Of Interest (AOI) or Point Of Interest (POI) has been
transformed in order
+ * to allow identification of wraparound axes. If {@code true}, then
{@link #geographicToAOI}
+ * needs to be applied in order to restore the AOI or POI to its original
projected CRS.
+ */
+ private boolean isResultTransformed;
+
+ /**
+ * Creates a new instance for adjusting an Area Of Interest (AOI) or Point
Of Interest (POI) to the given
+ * domain of validity. The AOI or POI will be given later, but this method
nevertheless requires in advance
+ * the transform from {@code domainOfValidity} to AOI or POI.
*
- * @param areaOfInterest the envelope to potentially shift toward the
domain of validity.
+ * @param domainOfValidity the region where a given area or point of
interest should be located.
+ * @param domainToAOI if the AOI or POI are going to use a
different CRS than {@code domainOfValidity}, the
+ * transform from {@code domainOfValidity} to
the AOI or POI CRS. Otherwise {@code null}.
+ * @param domainToAny a transform from the {@code domainOfValidity}
CRS to any user space at caller choice.
*/
- public WraparoundAdjustment(final Envelope areaOfInterest) {
- this.areaOfInterest = areaOfInterest;
+ public WraparoundAdjustment(final Envelope domainOfValidity, final
MathTransform domainToAOI, final MathTransform domainToAny) {
+ this.domainOfValidity = domainOfValidity;
+ this.domainToAOI = domainToAOI;
+ this.domainToAny = domainToAny;
}
/**
@@ -74,248 +121,333 @@ public final class WraparoundAdjustment {
* or {@link Double#NaN} otherwise. This method implements a fallback for
the longitude
* axis if it does not declare the minimum and maximum values as expected.
*
- * @param cs the coordinate system for which to get wraparound
range, or {@code null}.
+ * @param cs the coordinate system for which to get wraparound
range.
* @param dimension dimension of the axis to test.
* @return the wraparound range, or {@link Double#NaN} if none.
*/
static double range(final CoordinateSystem cs, final int dimension) {
- if (cs != null) {
- final CoordinateSystemAxis axis = cs.getAxis(dimension);
- if (axis != null &&
RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
- double period = axis.getMaximumValue() -
axis.getMinimumValue();
- if (period > 0 && period != Double.POSITIVE_INFINITY) {
- return period;
- }
- final AxisDirection dir =
AxisDirections.absolute(axis.getDirection());
- if (AxisDirection.EAST.equals(dir) && cs instanceof
EllipsoidalCS) {
- period = Longitude.MAX_VALUE - Longitude.MIN_VALUE;
- final Unit<?> unit = axis.getUnit();
- if (unit != null) {
- period =
Units.DEGREE.getConverterTo(Units.ensureAngular(unit)).convert(period);
- }
- return period;
+ final CoordinateSystemAxis axis = cs.getAxis(dimension);
+ if (axis != null &&
RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
+ double period = axis.getMaximumValue() - axis.getMinimumValue();
+ if (period > 0 && period != Double.POSITIVE_INFINITY) {
+ return period;
+ }
+ final AxisDirection dir =
AxisDirections.absolute(axis.getDirection());
+ if (AxisDirection.EAST.equals(dir) && cs instanceof EllipsoidalCS)
{
+ period = Longitude.MAX_VALUE - Longitude.MIN_VALUE;
+ final Unit<?> unit = axis.getUnit();
+ if (unit != null) {
+ period =
Units.DEGREE.getConverterTo(Units.ensureAngular(unit)).convert(period);
}
+ return period;
}
}
return Double.NaN;
}
/**
- * Computes an envelope with coordinates equivalent to the {@code
areaOfInterest} specified
- * at construction time, but potentially shifted for intersecting the
given domain of validity.
- * The dimensions that may be shifted are the ones having an axis with
wraparound meaning.
- * The envelope may have been converted to a geographic CRS for performing
this operation.
- *
- * <p>The coordinate reference system must be specified in the {@code
areaOfInterest}
- * specified at construction time, or (as a fallback) in the given {@code
domainOfValidity}.
- * If none of those envelopes have a CRS, then this method does
nothing.</p>
+ * Sets {@link #crs} to the given value if it is non-null, or to the
{@link #domainOfValidity} CRS otherwise.
+ * If no non-null CRS is available, returns {@code false} for instructing
caller to terminate immediately.
*
- * <p>This method does not intersect the area of interest with the domain
of validity.
- * It is up to the caller to compute that intersection after this method
call, if desired.</p>
- *
- * @param domainOfValidity the domain of validity, or {@code null} if
none.
- * @param validToAOI if the envelopes do not use the same CRS, the
transformation from {@code domainOfValidity}
- * to {@code areaOfInterest}. Otherwise {@code
null}. This method does not check by itself if
- * a coordinate operation is needed; it must be
supplied.
- * @return this object, allowing to chain {@link #result(MathTransform)}
operation.
- * @throws TransformException if an envelope transformation was required
but failed.
- *
- * @see GeneralEnvelope#simplify()
+ * @return whether a non-null CRS has been set.
*/
- public WraparoundAdjustment shiftInto(Envelope domainOfValidity,
MathTransform validToAOI) throws TransformException {
- CoordinateReferenceSystem crs =
areaOfInterest.getCoordinateReferenceSystem();
+ private boolean setIfNonNull(CoordinateReferenceSystem crs) {
if (crs == null) {
- crs = domainOfValidity.getCoordinateReferenceSystem(); //
Assumed to apply to AOI too.
+ assert domainToAOI == null || domainToAOI.isIdentity();
+ crs = domainOfValidity.getCoordinateReferenceSystem(); //
Assumed to apply to AOI or POI too.
if (crs == null) {
- return this;
+ return false;
}
}
- /*
- * If the coordinate reference system is a projected CRS, it will not
have any wraparound axis.
- * We need to perform the verification in its base geographic CRS
instead, and remember that we
- * may need to transform the result later.
- */
- final MathTransform projection;
- final DirectPosition lowerCorner;
- final DirectPosition upperCorner;
- GeneralEnvelope shifted = null; // To be initialized to a copy
of 'areaOfInterest' when first needed.
+ this.crs = crs;
+ return true;
+ }
+
+ /**
+ * If the coordinate reference system is a projected CRS, replaces it by
another CRS where wraparound axes can
+ * be identified. The wraparound axes are identifiable in base geographic
CRS. If such replacement is applied,
+ * remember that we may need to transform the result later.
+ *
+ * @return whether the replacement has been done. If {@code true}, then
{@link #geographicToAOI} is non-null.
+ */
+ private boolean replaceCRS() {
if (crs instanceof ProjectedCRS) {
final ProjectedCRS p = (ProjectedCRS) crs;
- crs = p.getBaseCRS();
- projection = p.getConversionFromBase().getMathTransform();
- shifted = Envelopes.transform(projection.inverse(),
areaOfInterest);
- lowerCorner = shifted.getLowerCorner();
- upperCorner = shifted.getUpperCorner();
- if (validToAOI == null) {
- validToAOI =
MathTransforms.identity(projection.getTargetDimensions());
- }
+ crs = p.getBaseCRS(); //
Geographic, so a wraparound axis certainly exists.
+ geographicToAOI = p.getConversionFromBase().getMathTransform();
+ return true;
} else {
- projection = null;
- lowerCorner = areaOfInterest.getLowerCorner();
- upperCorner = areaOfInterest.getUpperCorner();
+ return false;
}
- /*
- * We will not read 'areaOfInterest' anymore after we got its two
corner points.
- * The following loop search for "wraparound" axis.
- */
- final CoordinateSystem cs = crs.getCoordinateSystem();
- for (int i=cs.getDimension(); --i >= 0;) {
- final double period = range(cs, i);
- if (period > 0) {
- /*
- * Found an axis (typically the longitude axis) with
wraparound range meaning.
- * We are going to need the domain of validity in the same CRS
than the AOI.
- * Transform that envelope when first needed.
- */
- if (validToAOI != null) {
- if (projection != null) {
- validToAOI = MathTransforms.concatenate(validToAOI,
projection.inverse());
- }
- if (!validToAOI.isIdentity()) {
- domainOfValidity = Envelopes.transform(validToAOI,
domainOfValidity);
- }
- validToAOI = null;
- }
- /*
- * "Unroll" the range. For example if we have [+160 … -170]°
of longitude, we can replace by [160 … 190]°.
- * We do not change the 'lower' or 'upper' value now in order
to avoid rounding error. Instead we compute
- * how many periods we need to add to those values. We adjust
the side which results in the value closest
- * to zero, in order to reduce rounding error if no more
adjustment is done in the next block.
- */
- final double lower = lowerCorner.getOrdinate(i);
- final double upper = upperCorner.getOrdinate(i);
- double lowerCycles = 0; // In
number of periods.
- double upperCycles = 0;
- double delta = upper - lower;
- if (MathFunctions.isNegative(delta)) { // Use
'isNegative' for catching [+0 … -0] range.
- final double cycles = (delta == 0) ? -1 : Math.floor(delta
/ period); // Always negative.
- delta = cycles * period;
- if (Math.abs(lower + delta) < Math.abs(upper - delta)) {
- lowerCycles = cycles;
// Will subtract periods to 'lower'.
- } else {
- upperCycles = -cycles;
// Will add periods to 'upper'.
- }
- }
- /*
- * The range may be before or after the domain of validity.
Compute the distance from current
- * lower/upper coordinate to the coordinate of validity domain
(the sign tells us whether we
- * are before or after). The cases can be:
- *
- *
┌─────────────┬────────────┬────────────────────────────┬───────────────────────────────┐
- * │lowerIsBefore│upperIsAfter│ Meaning │
Action │
- *
├─────────────┼────────────┼────────────────────────────┼───────────────────────────────┤
- * │ false │ false │ AOI is inside valid area │
Nothing to do │
- * │ true │ true │ AOI encompasses valid area │
Nothing to do │
- * │ true │ false │ AOI on left of valid area │
Add positive amount of period │
- * │ false │ true │ AOI on right of valid area │
Add negative amount of period │
- *
└─────────────┴────────────┴────────────────────────────┴───────────────────────────────┘
- *
- * We try to compute multiples of 'periods' instead than just
adding or subtracting 'periods' once in
- * order to support images that cover more than one period,
for example images over 720° of longitude.
- * It may happen for example if an image shows data under the
trajectory of a satellite.
- */
- final double validStart =
domainOfValidity.getMinimum(i);
- final double validEnd =
domainOfValidity.getMaximum(i);
- final double lowerToValidStart = ((validStart - lower) /
period) - lowerCycles; // In number of periods.
- final double upperToValidEnd = ((validEnd - upper) /
period) - upperCycles;
- final boolean lowerIsBefore = (lowerToValidStart > 0);
- final boolean upperIsAfter = (upperToValidEnd < 0);
- if (lowerIsBefore != upperIsAfter) {
- final double upperToValidStart = ((validStart - upper) /
period) - upperCycles;
- final double lowerToValidEnd = ((validEnd - lower) /
period) - lowerCycles;
- if (lowerIsBefore) {
- /*
- * Notation: ⎣x⎦=floor(x) and ⎡x⎤=ceil(x).
- *
- * We need to add an integer amount of 'period' to
both sides in order to move the range
- * inside the valid area. We need ⎣lowerToValidStart⎦
for reaching the point where:
- *
- * (validStart - period) < (new lower) ≦ validStart
- *
- * But we may add more because there will be no
intersection without following condition:
- *
- * (new upper) ≧ validStart
- *
- * That second condition is met by
⎡upperToValidStart⎤. However adding more may cause the
- * range to move the AOI completely on the right side
of the domain of validity. We prevent
- * that with a third condition:
- *
- * (new lower) < validEnd
- */
- final double cycles =
Math.min(Math.floor(lowerToValidEnd),
-
Math.max(Math.floor(lowerToValidStart),
- Math.ceil
(upperToValidStart)));
- /*
- * If after the shift we see that the following
condition hold:
- *
- * (new lower) + period < validEnd
- *
- * Then we may have a situation like below:
- *
┌────────────────────────────────────────────┐
- * │ Domain of validity
│
- *
└────────────────────────────────────────────┘
- * ┌────────────────────┐
┌─────
- * │ Area of interest │
│ AOI
- * └────────────────────┘
└─────
- *
↖……………………………………………………………period……………………………………………………………↗︎
- *
- * The user may be requesting two extremums of the
domain of validity. We can not express
- * that with a single envelope. Instead, we will
expand the Area Of Interest to encompass
- * the full domain of validity.
- */
- if (cycles + 1 < lowerToValidEnd) {
- upperCycles += Math.ceil(upperToValidEnd);
+ }
+
+ /**
+ * Transforms {@link #domainOfValidity} to the same CRS than the Area Of
Interest (AOI) or Point Of Interest (POI).
+ * This method should be invoked only when the caller detected a
wraparound axis. This method transforms the domain
+ * the first time it is invoked, and does nothing on all subsequent calls.
+ */
+ private void transformDomainToAOI() throws TransformException {
+ if (!isDomainTransformed) {
+ isDomainTransformed = true;
+ MathTransform domainToGeographic = domainToAOI;
+ if (domainToGeographic == null) {
+ domainToGeographic = geographicToAOI;
+ } else if (geographicToAOI != null) {
+ domainToGeographic =
MathTransforms.concatenate(domainToGeographic, geographicToAOI.inverse());
+ }
+ if (domainToGeographic != null &&
!domainToGeographic.isIdentity()) {
+ domainOfValidity = Envelopes.transform(domainToGeographic,
domainOfValidity);
+ }
+ }
+ }
+
+ /**
+ * Returns the final transform to apply on the AOI or POI before to return
it to the user.
+ */
+ private MathTransform toFinal() throws TransformException {
+ MathTransform mt = domainToAny;
+ if (isResultTransformed && geographicToAOI != null) {
+ mt = MathTransforms.concatenate(geographicToAOI, mt);
+ }
+ return mt;
+ }
+
+ /**
+ * Computes an envelope with coordinates equivalent to the given {@code
areaOfInterest}, but
+ * potentially shifted for intersecting the domain of validity specified
at construction time.
+ * The dimensions that may be shifted are the ones having an axis with
wraparound meaning.
+ * In order to perform this operation, the envelope may be temporarily
converted to a geographic CRS
+ * and converted back to its original CRS.
+ *
+ * <p>The coordinate reference system should be specified in the {@code
areaOfInterest},
+ * or (as a fallback) in the {@code domainOfValidity} specified at
construction time.</p>
+ *
+ * <p>This method does not intersect the area of interest with the domain
of validity.
+ * It is up to the caller to compute that intersection after this method
call, if desired.</p>
+ *
+ * @param areaOfInterest the envelope to potentially shift toward domain
of validity.
+ * If a shift is needed, then given envelope will be replaced by a
new envelope;
+ * the given envelope will not be modified.
+ * @return envelope potentially expanded or shifted toward the domain of
validity.
+ * @throws TransformException if a coordinate conversion failed.
+ *
+ * @see GeneralEnvelope#simplify()
+ */
+ public GeneralEnvelope shift(Envelope areaOfInterest) throws
TransformException {
+ if (setIfNonNull(areaOfInterest.getCoordinateReferenceSystem())) {
+ /*
+ * If the coordinate reference system is a projected CRS, it will
not have any wraparound axis.
+ * We need to perform the verification in its base geographic CRS
instead, and remember that we
+ * may need to transform the result later.
+ */
+ final DirectPosition lowerCorner;
+ final DirectPosition upperCorner;
+ GeneralEnvelope shifted; // To be initialized to a copy
of 'areaOfInterest' when first needed.
+ if (replaceCRS()) {
+ shifted = Envelopes.transform(geographicToAOI.inverse(),
areaOfInterest);
+ lowerCorner = shifted.getLowerCorner();
+ upperCorner = shifted.getUpperCorner();
+ } else {
+ shifted = null;
+ lowerCorner = areaOfInterest.getLowerCorner();
+ upperCorner = areaOfInterest.getUpperCorner();
+ }
+ /*
+ * We will not read 'areaOfInterest' anymore after we got its two
corner points (except for creating
+ * a copy if `shifted` is still null). The following loop searches
for "wraparound" axes.
+ */
+ final CoordinateSystem cs = crs.getCoordinateSystem();
+ for (int i=cs.getDimension(); --i >= 0;) {
+ final double period = range(cs, i);
+ if (period > 0) {
+ /*
+ * Found an axis (typically the longitude axis) with
wraparound range meaning.
+ * We are going to need the domain of validity in the same
CRS than the AOI.
+ * Transform that envelope when first needed.
+ */
+ transformDomainToAOI();
+ /*
+ * "Unroll" the range. For example if we have [+160 …
-170]° of longitude, we can replace by [160 … 190]°.
+ * We do not change the 'lower' or 'upper' value now in
order to avoid rounding error. Instead we compute
+ * how many periods we need to add to those values. We
adjust the side which results in the value closest
+ * to zero, in order to reduce rounding error if no more
adjustment is done in the next block.
+ */
+ final double lower = lowerCorner.getOrdinate(i);
+ final double upper = upperCorner.getOrdinate(i);
+ double lowerCycles = 0; // In
number of periods.
+ double upperCycles = 0;
+ double delta = upper - lower;
+ if (MathFunctions.isNegative(delta)) { // Use
'isNegative' for catching [+0 … -0] range.
+ final double cycles = (delta == 0) ? -1 :
Math.floor(delta / period); // Always negative.
+ delta = cycles * period;
+ if (Math.abs(lower + delta) < Math.abs(upper - delta))
{
+ lowerCycles = cycles;
// Will subtract periods to 'lower'.
} else {
- upperCycles += cycles;
+ upperCycles = -cycles;
// Will add periods to 'upper'.
}
- lowerCycles += cycles;
- } else {
- /*
- * Same reasoning than above with sign reverted and
lower/upper variables interchanged.
- * In this block, 'upperToValidEnd' and
'lowerToValidEnd' are negative, contrarily to
- * above block where they were positive.
- */
- final double cycles = Math.max(Math.ceil
(upperToValidStart),
- Math.min(Math.ceil
(upperToValidEnd),
-
Math.floor(lowerToValidEnd)));
- if (cycles - 1 > upperToValidStart) {
- lowerCycles += Math.floor(lowerToValidStart);
- } else {
+ }
+ /*
+ * The range may be before or after the domain of
validity. Compute the distance from current
+ * lower/upper coordinate to the coordinate of validity
domain (the sign tells us whether we
+ * are before or after). The cases can be:
+ *
+ *
┌─────────────┬────────────┬────────────────────────────┬───────────────────────────────┐
+ * │lowerIsBefore│upperIsAfter│ Meaning
│ Action │
+ *
├─────────────┼────────────┼────────────────────────────┼───────────────────────────────┤
+ * │ false │ false │ AOI is inside valid area
│ Nothing to do │
+ * │ true │ true │ AOI encompasses valid
area │ Nothing to do │
+ * │ true │ false │ AOI on left of valid
area │ Add positive amount of period │
+ * │ false │ true │ AOI on right of valid
area │ Add negative amount of period │
+ *
└─────────────┴────────────┴────────────────────────────┴───────────────────────────────┘
+ *
+ * We try to compute multiples of 'periods' instead than
just adding or subtracting 'periods' once in
+ * order to support images that cover more than one
period, for example images over 720° of longitude.
+ * It may happen for example if an image shows data under
the trajectory of a satellite.
+ */
+ final double validStart =
domainOfValidity.getMinimum(i);
+ final double validEnd =
domainOfValidity.getMaximum(i);
+ final double lowerToValidStart = ((validStart - lower) /
period) - lowerCycles; // In number of periods.
+ final double upperToValidEnd = ((validEnd - upper) /
period) - upperCycles;
+ final boolean lowerIsBefore = (lowerToValidStart > 0);
+ final boolean upperIsAfter = (upperToValidEnd < 0);
+ if (lowerIsBefore != upperIsAfter) {
+ final double upperToValidStart = ((validStart - upper)
/ period) - upperCycles;
+ final double lowerToValidEnd = ((validEnd - lower)
/ period) - lowerCycles;
+ if (lowerIsBefore) {
+ /*
+ * Notation: ⎣x⎦=floor(x) and ⎡x⎤=ceil(x).
+ *
+ * We need to add an integer amount of 'period' to
both sides in order to move the range
+ * inside the valid area. We need
⎣lowerToValidStart⎦ for reaching the point where:
+ *
+ * (validStart - period) < (new lower) ≦
validStart
+ *
+ * But we may add more because there will be no
intersection without following condition:
+ *
+ * (new upper) ≧ validStart
+ *
+ * That second condition is met by
⎡upperToValidStart⎤. However adding more may cause the
+ * range to move the AOI completely on the right
side of the domain of validity. We prevent
+ * that with a third condition:
+ *
+ * (new lower) < validEnd
+ */
+ final double cycles =
Math.min(Math.floor(lowerToValidEnd),
+
Math.max(Math.floor(lowerToValidStart),
+ Math.ceil
(upperToValidStart)));
+ /*
+ * If after the shift we see that the following
condition hold:
+ *
+ * (new lower) + period < validEnd
+ *
+ * Then we may have a situation like below:
+ *
┌────────────────────────────────────────────┐
+ * │ Domain of
validity │
+ *
└────────────────────────────────────────────┘
+ * ┌────────────────────┐
┌─────
+ * │ Area of interest │
│ AOI
+ * └────────────────────┘
└─────
+ *
↖……………………………………………………………period……………………………………………………………↗︎
+ *
+ * The user may be requesting two extremums of the
domain of validity. We can not express
+ * that with a single envelope. Instead, we will
expand the Area Of Interest to encompass
+ * the full domain of validity.
+ */
+ if (cycles + 1 < lowerToValidEnd) {
+ upperCycles += Math.ceil(upperToValidEnd);
+ } else {
+ upperCycles += cycles;
+ }
lowerCycles += cycles;
+ } else {
+ /*
+ * Same reasoning than above with sign reverted
and lower/upper variables interchanged.
+ * In this block, 'upperToValidEnd' and
'lowerToValidEnd' are negative, contrarily to
+ * above block where they were positive.
+ */
+ final double cycles = Math.max(Math.ceil
(upperToValidStart),
+ Math.min(Math.ceil
(upperToValidEnd),
+
Math.floor(lowerToValidEnd)));
+ if (cycles - 1 > upperToValidStart) {
+ lowerCycles += Math.floor(lowerToValidStart);
+ } else {
+ lowerCycles += cycles;
+ }
+ upperCycles += cycles;
}
- upperCycles += cycles;
}
- }
- /*
- * If there is change to apply, copy the envelope when first
needed and set the fields.
- * If we never enter in this block, then 'areaOfInterest' will
stay the envelope given
- * at construction time.
- */
- if (lowerCycles != 0 || upperCycles != 0) {
- if (shifted == null) {
- shifted = new GeneralEnvelope(areaOfInterest);
+ /*
+ * If there is change to apply, copy the envelope when
first needed and set the fields.
+ * If we never enter in this block, then 'areaOfInterest'
will stay the envelope given
+ * at construction time.
+ */
+ if (lowerCycles != 0 || upperCycles != 0) {
+ isResultTransformed = true;
+ if (shifted == null) {
+ shifted = new GeneralEnvelope(areaOfInterest);
+ }
+ areaOfInterest = shifted; //
'shifted' may have been set before the loop.
+ shifted.setRange(i, lower + lowerCycles * period, //
TODO: use Math.fma in JDK9.
+ upper + upperCycles * period);
}
- areaOfInterest = shifted; //
'shifted' may have been set before the loop.
- geographicToAOI = projection;
- shifted.setRange(i, lower + lowerCycles * period, //
TODO: use Math.fma in JDK9.
- upper + upperCycles * period);
}
}
}
- return this;
+ return Envelopes.transform(toFinal(), areaOfInterest);
}
/**
- * Returns the (potentially shifted and/or expanded) area of interest
converted by the given transform.
+ * Computes a position with coordinates equivalent to the given {@code
pointOfInterest}, but
+ * potentially shifted to interior of the domain of validity specified at
construction time.
+ * The dimensions that may be shifted are the ones having an axis with
wraparound meaning.
+ * In order to perform this operation, the position may be temporarily
converted to a geographic CRS
+ * and converted back to its original CRS.
+ *
+ * <p>The coordinate reference system should be specified in the {@code
pointOfInterest},
+ * or (as a fallback) in the {@code domainOfValidity} specified at
construction time.</p>
*
- * @param mt a transform from the CRS of the {@code areaOfInterest}
given to the constructor.
- * @return the area of interest transformed by the given {@code
MathTransform}.
- * @throws TransformException if the transformation failed.
+ * @param pointOfInterest the position to potentially shift to domain of
validity interior.
+ * If a shift is needed, then the given position will be replaced
by a new position;
+ * the given position will not be modified.
+ * @return position potentially shifted to the domain of validity interior.
+ * @throws TransformException if a coordinate conversion failed.
*/
- public GeneralEnvelope result(MathTransform mt) throws TransformException {
- if (geographicToAOI != null) {
- mt = MathTransforms.concatenate(geographicToAOI, mt);
+ public DirectPosition shift(DirectPosition pointOfInterest) throws
TransformException {
+ if (setIfNonNull(pointOfInterest.getCoordinateReferenceSystem())) {
+ DirectPosition shifted;
+ if (replaceCRS()) {
+ shifted = geographicToAOI.inverse().transform(pointOfInterest,
null);
+ } else {
+ shifted = pointOfInterest; // To be replaced by a
copy of 'pointOfInterest' when first needed.
+ }
+ final CoordinateSystem cs = crs.getCoordinateSystem();
+ for (int i=cs.getDimension(); --i >= 0;) {
+ final double period = range(cs, i);
+ if (period > 0) {
+ transformDomainToAOI();
+ final double x = shifted.getOrdinate(i);
+ double delta = domainOfValidity.getMinimum(i) - x;
+ if (delta > 0) { //
Test for point before domain of validity.
+ delta = Math.ceil(delta / period);
+ } else {
+ delta = domainOfValidity.getMaximum(i) - x;
+ if (delta < 0) { //
Test for point after domain of validity.
+ delta = Math.floor(delta / period);
+ } else {
+ continue;
+ }
+ }
+ if (delta != 0) {
+ isResultTransformed = true;
+ if (shifted == pointOfInterest) {
+ shifted = new
GeneralDirectPosition(pointOfInterest);
+ }
+ pointOfInterest = shifted; //
'shifted' may have been set before the loop.
+ shifted.setOrdinate(i, x + delta * period); //
TODO: use Math.fma in JDK9.
+ }
+ }
+ }
}
- return Envelopes.transform(mt, areaOfInterest);
+ return toFinal().transform(pointOfInterest, null);
}
}
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
index ba454f5..813ccdb 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
@@ -58,13 +58,12 @@ public final strictfp class WraparoundAdjustmentTest
extends TestCase {
private static Envelope adjustWraparoundAxes(Envelope areaOfInterest,
Envelope domainOfValidity, MathTransform validToAOI)
throws TransformException
{
- WraparoundAdjustment adj = new WraparoundAdjustment(areaOfInterest);
- adj.shiftInto(domainOfValidity, validToAOI);
- return adj.result(MathTransforms.identity(2));
+ WraparoundAdjustment adj = new WraparoundAdjustment(domainOfValidity,
validToAOI, MathTransforms.identity(2));
+ return adj.shift(areaOfInterest);
}
/**
- * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)}
+ * Tests {@link WraparoundAdjustment#shift(Envelope)}
* with an envelope crossing the anti-meridian.
*
* @throws TransformException should never happen since this test does not
transform coordinates.
@@ -88,7 +87,7 @@ public final strictfp class WraparoundAdjustmentTest extends
TestCase {
}
/**
- * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)}
+ * Tests {@link WraparoundAdjustment#shift(Envelope)}
* with an envelope which is outside the grid valid area.
*
* @throws TransformException should never happen since this test does not
transform coordinates.
@@ -108,7 +107,7 @@ public final strictfp class WraparoundAdjustmentTest
extends TestCase {
}
/**
- * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)}
+ * Tests {@link WraparoundAdjustment#shift(Envelope)}
* with an envelope shifted by 360° before or after the grid valid area.
*
* @throws TransformException should never happen since this test does not
transform coordinates.
@@ -144,7 +143,7 @@ public final strictfp class WraparoundAdjustmentTest
extends TestCase {
}
/**
- * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)}
+ * Tests {@link WraparoundAdjustment#shift(Envelope)}
* with an envelope that cause the method to expand the area of interest.
Illustration:
*
* {@preformat text
@@ -178,7 +177,7 @@ public final strictfp class WraparoundAdjustmentTest
extends TestCase {
}
/**
- * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)}
with a projected envelope.
+ * Tests {@link WraparoundAdjustment#shift(Envelope)} with a projected
envelope.
*
* @throws TransformException if an error occurred while projecting a
coordinate.
*/