You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/02/18 19:13:19 UTC
[sis] branch geoapi-4.0 updated: Revisit `WraparoundAdjustment` before move in public API.
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 d9dc1f9 Revisit `WraparoundAdjustment` before move in public API.
d9dc1f9 is described below
commit d9dc1f9306a95e6f25f5d085d3f6ccc646a06620
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Feb 18 19:58:20 2022 +0100
Revisit `WraparoundAdjustment` before move in public API.
---
.../apache/sis/coverage/grid/GridDerivation.java | 64 ++--
.../sis/coverage/grid/WraparoundAdjustment.java | 412 +++++++++++++++------
.../coverage/grid/WraparoundAdjustmentTest.java | 5 +-
3 files changed, 333 insertions(+), 148 deletions(-)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index 0822d14..012d716 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -28,7 +28,6 @@ import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.LinearTransform;
@@ -602,17 +601,8 @@ public class GridDerivation {
final CoordinateReferenceSystem crs = areaOfInterest.getCoordinateReferenceSystem();
if (crs != null) {
areaOfInterest = new DimensionReducer(base, crs).apply(areaOfInterest);
- CoordinateOperation op = Envelopes.findOperation(base.envelope, areaOfInterest);
- if (op == null) {
- /*
- * If above call to `Envelopes.findOperation(…)` failed, then `base.envelope` CRS is probably null.
- * Try with a call to `getCoordinateReferenceSystem()` for throwing IncompleteGridGeometryException,
- * unless the user overrode that method in which case we will use its value.
- */
- op = CRS.findOperation(base.getCoordinateReferenceSystem(), crs, null);
- }
- baseToAOI = op.getMathTransform();
- cornerToCRS = MathTransforms.concatenate(cornerToCRS, baseToAOI);
+ baseToAOI = findBaseToAOI(areaOfInterest.getCoordinateReferenceSystem());
+ cornerToCRS = MathTransforms.concatenate(cornerToCRS, baseToAOI);
}
}
/*
@@ -644,7 +634,7 @@ public class GridDerivation {
dimension = baseExtent.getDimension(); // Non-null since `base.requireGridToCRS()` succeed.
GeneralEnvelope indices = null;
if (areaOfInterest != null) {
- indices = new WraparoundAdjustment(base.envelope, baseToAOI, cornerToCRS.inverse()).shift(areaOfInterest);
+ indices = wraparound(baseToAOI, cornerToCRS).shift(areaOfInterest);
setBaseExtentClipped(indices);
}
if (indices == null || indices.getDimension() != dimension) {
@@ -732,25 +722,48 @@ public class GridDerivation {
}
/**
+ * Returns the transform from the CRS of the {@linkplain #base} grid to the CRS of user-supplied argument.
+ *
+ * @param target the CRS of the user-supplied argument (envelope ou position).
+ * @return transform from {@linkplain #base} grid to user argument.
+ */
+ private MathTransform findBaseToAOI(final CoordinateReferenceSystem target) throws FactoryException {
+ final CoordinateReferenceSystem gridCRS = base.getCoordinateReferenceSystem(); // May throw exception.
+ return CRS.findOperation(gridCRS, target, base.getGeographicExtent().orElse(null)).getMathTransform();
+ }
+
+ /**
+ * Creates an instance of the helper class for shifting positions and envelopes inside the grid.
+ *
+ * @param baseToAOI the transform computed by {@link #findBaseToAOI(CoordinateReferenceSystem)},
+ * or {@code null} if same as the CRS of the {@linkplain #base} grid geometry.
+ * @param gridToCRS the transform computed by {@link #dropUnusedDimensions(MathTransform, int)}
+ * (the transform from grid coordinates to the CRS of user-supplied AOI/POI).
+ */
+ private WraparoundAdjustment wraparound(MathTransform baseToAOI, MathTransform gridToCRS) throws TransformException {
+ return new WraparoundAdjustment(base.envelope, baseToAOI, gridToCRS.inverse());
+ }
+
+ /**
* Drops the source dimensions that are not needed for producing the target dimensions.
* The retained source dimensions are stored in {@link #modifiedDimensions}.
* This method is invoked in an effort to make the transform invertible.
*
- * @param cornerToCRS transform from grid coordinates to AOI coordinates.
- * @param dimension value of {@code cornerToCRS.getTargetDimensions()}.
+ * @param gridToCRS transform from grid coordinates to AOI coordinates.
+ * @param dimension value of {@code cornerToCRS.getTargetDimensions()}.
*/
- private MathTransform dropUnusedDimensions(MathTransform cornerToCRS, final int dimension)
+ private MathTransform dropUnusedDimensions(MathTransform gridToCRS, final int dimension)
throws FactoryException, TransformException
{
- if (dimension < cornerToCRS.getSourceDimensions()) {
- final TransformSeparator sep = new TransformSeparator(cornerToCRS);
- cornerToCRS = sep.separate();
+ if (dimension < gridToCRS.getSourceDimensions()) {
+ final TransformSeparator sep = new TransformSeparator(gridToCRS);
+ gridToCRS = sep.separate();
modifiedDimensions = sep.getSourceDimensions();
if (modifiedDimensions.length != dimension) {
throw new TransformException(Resources.format(Resources.Keys.CanNotMapToGridDimensions));
}
}
- return cornerToCRS;
+ return gridToCRS;
}
/**
@@ -946,8 +959,8 @@ public class GridDerivation {
* 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.
+ * @param slicePoint the coordinates where to get a slice. If no coordinate reference system is associated,
+ * this method assumes that the slice point CRS is the CRS of the 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 no CRS (unless {@code slicePoint} has no CRS neither, in which case the CRS are assumed the same).
@@ -974,9 +987,8 @@ public class GridDerivation {
baseToPOI = null;
} else {
slicePoint = new DimensionReducer(base, sliceCRS).apply(slicePoint);
- final CoordinateReferenceSystem gridCRS = base.getCoordinateReferenceSystem(); // May throw exception.
- baseToPOI = CRS.findOperation(gridCRS, sliceCRS, null).getMathTransform();
- gridToCRS = MathTransforms.concatenate(gridToCRS, baseToPOI);
+ baseToPOI = findBaseToAOI(sliceCRS);
+ gridToCRS = MathTransforms.concatenate(gridToCRS, baseToPOI);
}
/*
* If the point dimensions do not encompass all grid dimensions, the transform is probably non-invertible.
@@ -990,7 +1002,7 @@ public class GridDerivation {
* 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);
+ DirectPosition gridPoint = wraparound(baseToPOI, gridToCRS).shift(slicePoint);
if (scaledExtent != null) {
scaledExtent = scaledExtent.slice(gridPoint, modifiedDimensions);
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/WraparoundAdjustment.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/WraparoundAdjustment.java
index 32017c2..a089757 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/WraparoundAdjustment.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/WraparoundAdjustment.java
@@ -18,162 +18,323 @@ package org.apache.sis.coverage.grid;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
+import org.opengis.util.FactoryException;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.metadata.extent.GeographicBoundingBox;
import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.CRS;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.geometry.AbstractEnvelope;
+import org.apache.sis.internal.metadata.ReferencingServices;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.internal.referencing.WraparoundApplicator;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Utilities;
/**
- * Adjustments applied on an envelope for handling wraparound axes. The adjustments consist in shifting
- * 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.
+ * An envelope or position converter making them more compatible with a given domain of validity.
+ * For each axes having {@link org.opengis.referencing.cs.RangeMeaning#WRAPAROUND},
+ * this class can add or subtract an integer amount of periods (typically 360° of longitude)
+ * in attempt to move positions or envelopes inside a domain of validity specified at construction time.
+ *
+ * <p>{@code WraparoundAdjustment} instances are not thread-safe.</p>
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
final class WraparoundAdjustment {
/**
* 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.
+ * This domain is specified at construction time and does not change.
+ */
+ private final ImmutableEnvelope domainOfValidity;
+
+ /**
+ * The domain of validity transformed to a CRS where wraparound axes exist, or {@code null} if not yet computed.
+ * For example if {@link #domainOfValidity} is expressed in a projected CRS, then this envelope will be the same
+ * domain but converted to the base geographic CRS in order to allow identification of wraparound axes.
*/
- private Envelope domainOfValidity;
+ private AbstractEnvelope shiftableDomain;
/**
- * 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}.
+ * The geographic bounds of {@link #domainOfValidity}, or {@code null} if not applicable.
*
- * <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>
+ * @see CRS#findOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, GeographicBoundingBox)
*/
- private final MathTransform domainToAOI;
+ private GeographicBoundingBox geographicDomain;
/**
- * 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
+ * Whether {@link #geographicDomain} has been computed (result may be null).
+ */
+ private boolean geographicDomainKnown;
+
+ /**
+ * Coordinate reference system of the last Area Of Interest (AOI) or Point Of Interest (POI).
+ * This is used for detecting when the input CRS changed.
+ */
+ private CoordinateReferenceSystem inputCRS;
+
+ /**
+ * Coordinate reference system of results, or {@code null} if unspecified.
+ */
+ private final CoordinateReferenceSystem resultCRS;
+
+ /**
+ * A transform from the {@link #inputCRS} to any destination user space at caller choice.
+ * Objects returned by {@code shift(…)} methods 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.
+ *
+ * <p>This field should be considered final if {@link #domainToInput} is non-null.</p>
*/
- private final MathTransform domainToAny;
+ private MathTransform inputToResult;
/**
- * If {@code areaOfInterest} or {@code pointOfInterest} has been converted to a geographic CRS,
- * the transformation back to its original CRS. Otherwise {@code null}.
+ * A transform from the {@link #domainOfValidity} CRS to the {@link #inputCRS} if it was explicitly specified,
+ * or {@code null} otherwise. If non-null, all input envelopes or positions will be assumed in the CRS which
+ * is the target of this transform. For performance reason, this assumption will not be verified.
*/
- private MathTransform geographicToAOI;
+ private final MathTransform domainToInput;
/**
- * 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.
+ * If the input envelopes or positions need to be converted to a (usually) geographic CRS,
+ * the transform to that CRS. Otherwise an identity transform. This is computed when first needed.
*/
- private CoordinateReferenceSystem crs;
+ private MathTransform inputToShiftable;
/**
- * 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.
+ * The transform from the intermediate CRS to final objects, computed when first needed.
+ *
+ * @see #toResult(boolean)
*/
- private boolean isDomainTransformed;
+ private MathTransform shiftableToResult;
/**
- * 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.
+ * The span (maximum - minimum) of wraparound axes, with 0 value for axes that are not wraparound.
+ * Initially null and computed when first needed. The length of this array may be shorter than the
+ * CRS number of dimensions if all remaining axes are not wraparound axes.
*/
- private boolean isResultTransformed;
+ private double[] periods;
/**
- * 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.
+ * Creates a new instance for adjusting Area Of Interest (AOI) or Point Of Interest (POI) to the given domain.
+ * The results of {@code shift(…)} methods will be transformed (if needed) to the specified CRS.
*
- * @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.
+ * @param domain the region where a given area or point of interest should be located.
+ * @param target the coordinate reference system of objects returned by {@code shift(…)} methods,
+ * or {@code null} for the same CRS than the {@code domain} CRS..
*/
- public WraparoundAdjustment(final Envelope domainOfValidity, final MathTransform domainToAOI, final MathTransform domainToAny) {
- this.domainOfValidity = domainOfValidity;
- this.domainToAOI = domainToAOI;
- this.domainToAny = domainToAny;
+ public WraparoundAdjustment(final Envelope domain, final CoordinateReferenceSystem target) {
+ ArgumentChecks.ensureNonNull("domain", domain);
+ domainOfValidity = ImmutableEnvelope.castOrCopy(domain);
+ resultCRS = (target != null) ? target : domainOfValidity.getCoordinateReferenceSystem();
+ domainToInput = null;
}
/**
- * 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.
+ * Creates a new instance with specified transforms from domain to the CRS of inputs, then to the CRS of outputs.
+ * This constructor can be used when those transforms are known in advance; it avoids the cost of inferring them.
+ * With this constructor, {@code WraparoundAdjustment} does <strong>not</strong> verify if a coordinate operation
+ * is needed for a pair of CRS; it is caller's responsibility to ensure that input objects use the expected CRS.
*
- * @return whether a non-null CRS has been set.
+ * <div class="note"><b>Example:</b>
+ * in the context of {@link org.apache.sis.coverage.grid.GridGeometry}, the {@code domain} argument may be the
+ * geospatial envelope of the grid and the {@code inputToResult} argument may be the "CRS to grid" transform.
+ * This configuration allows to compute grid coordinates having more chances to be inside the grid.</div>
+ *
+ * @param domain the region where a given area (AOI) or point of interest (POI) should be located.
+ * @param domainToInput if the AOI or POI will use a different CRS than {@code domain}, the transform from
+ * {@code domain} to the input CRS. Otherwise {@code null} for same CRS as the domain.
+ * @param inputToResult a transform from the {@code domain} CRS to any user space at caller choice.
+ * If {@code null}, the results will be expressed in same CRS than the inputs.
*/
- private boolean setIfNonNull(CoordinateReferenceSystem crs) {
- if (crs == null) {
- assert domainToAOI == null || domainToAOI.isIdentity();
- crs = domainOfValidity.getCoordinateReferenceSystem(); // Assumed to apply to AOI or POI too.
- if (crs == null) {
- return false;
- }
+ public WraparoundAdjustment(final Envelope domain, MathTransform domainToInput, MathTransform inputToResult) {
+ ArgumentChecks.ensureNonNull("domain", domain);
+ domainOfValidity = ImmutableEnvelope.castOrCopy(domain);
+ if (domainToInput == null) {
+ domainToInput = MathTransforms.identity(domainOfValidity.getDimension());
+ }
+ if (inputToResult == null) {
+ inputToResult = MathTransforms.identity(domainToInput.getTargetDimensions());
}
- this.crs = crs;
- return true;
+ this.domainToInput = domainToInput;
+ this.inputToResult = inputToResult;
+ this.resultCRS = null; // Not used by this instance.
}
/**
- * 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.
+ * Finds a coordinate operation from the given source CRS to target CRS.
+ * This method is invoked by all codes that need to find a coordinate operation.
*
- * @return whether the replacement has been done. If {@code true}, then {@link #geographicToAOI} is non-null.
+ * @param source the source CRS of the desired coordinate operation.
+ * @param target the target CRS of the desired coordinate operation.
+ * @return operation from {@code source} to {@code target}.
+ * @throws TransformException if the operation can not be computed.
*/
- private boolean replaceCRS() {
- if (crs instanceof ProjectedCRS) {
- final ProjectedCRS p = (ProjectedCRS) crs;
- crs = p.getBaseCRS(); // Geographic, so a wraparound axis certainly exists.
- geographicToAOI = p.getConversionFromBase().getMathTransform();
- return true;
- } else {
- return false;
+ private CoordinateOperation findOperation(final CoordinateReferenceSystem source,
+ final CoordinateReferenceSystem target)
+ throws TransformException
+ {
+ /*
+ * The (source ≉ target) condition is a quick check for avoiding
+ * unnecessary calculation of `geographicDomain` in common cases.
+ */
+ if (!geographicDomainKnown && !Utilities.equalsIgnoreMetadata(source, target)) try {
+ geographicDomainKnown = true; // Shall be set even in case of failure.
+ geographicDomain = ReferencingServices.getInstance().setBounds(domainOfValidity, null, null);
+ } catch (TransformException e) {
+ Logging.ignorableException(Logging.getLogger(Loggers.COORDINATE_OPERATION), WraparoundAdjustment.class, "<init>", e);
+ // No more attempt will be done.
+ }
+ try {
+ return CRS.findOperation(source, target, geographicDomain);
+ } catch (FactoryException e) {
+ throw new TransformException(e);
}
}
/**
- * 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.
+ * Initializes this {@code WraparoundAdjustment} for an AOI or POI having the given coordinate reference system.
+ * If the given CRS is the same than the CRS given in last call to this method, then this method does nothing as
+ * this {@code WraparoundAdjustment} is assumed already initialized. Otherwise this method performs those steps:
+ *
+ * <ul>
+ * <li>If the given coordinate reference system is a projected CRS,
+ * replaces it by another CRS where wraparound axes can be identified.</li>
+ * <li>Set {@link #shiftableDomain} to an envelope in above CRS.</li>
+ * <li>Set {@link #periods} to an array with the periods of wraparound axes.</li>
+ * <li>Set {@link #inputToResult} to the final transform to apply in {@code shift(…)} methods.</li>
+ * </ul>
+ *
+ * @return whether there is at least one wraparound axis.
*/
- 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());
+ private boolean initialize(CoordinateReferenceSystem crs) throws TransformException {
+ if (crs == null) {
+ crs = domainOfValidity.getCoordinateReferenceSystem();
+ if (crs == null && domainToInput == null) {
+ /*
+ * If `inputCRS` is also null, `inputToResult` will not be initialized by next block.
+ * Initialize here with the assumption that following CRS as same as domain CRS:
+ *
+ * - Input CRS, as specified in `shift(…)` method contract.
+ * - Result CRS, as specified in constructor contract.
+ *
+ * Note that this field is considered modifiable only if `domainToInput` is null (see its javadoc).
+ */
+ inputToResult = MathTransforms.identity(domainOfValidity.getDimension());
}
- if (domainToGeographic != null && !domainToGeographic.isIdentity()) {
- domainOfValidity = Envelopes.transform(domainToGeographic, domainOfValidity);
+ }
+ if (crs != inputCRS) {
+ inputCRS = crs;
+ periods = null;
+ inputToShiftable = null; // Will not be used if `crs` is null.
+ shiftableToResult = null;
+ shiftableDomain = domainOfValidity;
+ /*
+ * Get the transform from input CRS (before replacement by "shiftable" CRS)
+ * to the CRS of all results. It will be needed by all `shift(…)` methods.
+ * Note that by convention, `inputToResult` is considered modifiable only
+ * if `domainToInput` is null (see its javadoc).
+ */
+ if (domainToInput == null) {
+ if (crs != null && resultCRS != null) {
+ inputToResult = findOperation(crs, resultCRS).getMathTransform();
+ } else {
+ inputToResult = MathTransforms.identity(
+ (crs != null) ? ReferencingUtilities.getDimension(crs)
+ : domainOfValidity.getDimension());
+ }
+ }
+ /*
+ * At this point we got a CRS which may have wraparound axes. Search for those axes.
+ * The `periods` array will become non-null only if we find at least one such axis.
+ */
+ if (crs != null) {
+ /*
+ * Replace the input CRS by an intermediate CRS where wraparound axes can be found.
+ * We try to select a CRS as close as possible (simplest transform) to the input.
+ */
+ if (crs instanceof ProjectedCRS) {
+ final ProjectedCRS p = (ProjectedCRS) crs;
+ crs = p.getBaseCRS(); // Geographic, so a wraparound axis certainly exists.
+ inputToShiftable = p.getConversionFromBase().getMathTransform().inverse();
+ } else {
+ // TODO: we should handle the case of CompoundCRS before to fallback on identity.
+ inputToShiftable = MathTransforms.identity(ReferencingUtilities.getDimension(crs));
+ }
+ final CoordinateSystem cs = crs.getCoordinateSystem();
+ for (int i = cs.getDimension(); --i >= 0;) {
+ final double period = WraparoundApplicator.range(cs, i);
+ if (period > 0) {
+ if (periods == null) {
+ transformDomain(crs);
+ periods = new double[i + 1];
+ }
+ periods[i] = period;
+ }
+ }
+ }
+ }
+ return (periods != null);
+ }
+
+ /**
+ * Transforms {@link #domainOfValidity} to a CRS where wraparound axes can be identified.
+ * This method should be invoked only when the caller detected at least one wraparound axis.
+ *
+ * <p>If a {@link #domainToInput} has been explicitly specified to the constructor,
+ * that transform is unconditionally used and the {@code crs} argument is ignored.</p>
+ *
+ * <h4>Preconditions</h4>
+ * <ul>
+ * <li>The {@link #inputToShiftable} transform must be initialized.</li>
+ * <li>The {@link #shiftableDomain} field is assumed initialized to {@link #domainOfValidity}.</li>
+ * </ul>
+ */
+ private void transformDomain(final CoordinateReferenceSystem target) throws TransformException {
+ final MathTransform domainToShiftable;
+ if (domainToInput != null) {
+ // Case of the constructor with `MathTransform` arguments.
+ domainToShiftable = MathTransforms.concatenate(domainToInput, inputToShiftable);
+ if (!domainToShiftable.isIdentity()) {
+ shiftableDomain = Envelopes.transform(domainToShiftable, domainOfValidity);
+ }
+ } else {
+ // Case of the constructor with `CoordinateReferenceSystem` argument.
+ CoordinateOperation op = findOperation(domainOfValidity.getCoordinateReferenceSystem(), target);
+ domainToShiftable = op.getMathTransform();
+ if (!domainToShiftable.isIdentity()) {
+ shiftableDomain = Envelopes.transform(op, domainOfValidity);
}
}
}
/**
* Returns the final transform to apply on the AOI or POI before to return it to the user.
+ * If {@link #inputCRS} is null, returns {@code null} for meaning "unknown transform".
*/
- private MathTransform toFinal() throws TransformException {
- MathTransform mt = domainToAny;
- if (isResultTransformed && geographicToAOI != null) {
- mt = MathTransforms.concatenate(geographicToAOI, mt);
+ private MathTransform toResult(final boolean isResultShifted) throws TransformException {
+ if (isResultShifted) {
+ if (shiftableToResult == null) {
+ shiftableToResult = MathTransforms.concatenate(inputToShiftable.inverse(), inputToResult);
+ }
+ return shiftableToResult;
+ } else {
+ return inputToResult;
}
- return mt;
}
/**
@@ -183,8 +344,8 @@ final class WraparoundAdjustment {
* 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>The coordinate reference system should be specified in the {@code areaOfInterest}.
+ * If not, then the CRS is assumed same as the CRS of the domain 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>
@@ -198,7 +359,8 @@ final class WraparoundAdjustment {
* @see GeneralEnvelope#simplify()
*/
public GeneralEnvelope shift(Envelope areaOfInterest) throws TransformException {
- if (setIfNonNull(areaOfInterest.getCoordinateReferenceSystem())) {
+ boolean isResultShifted = false;
+ if (initialize(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
@@ -207,30 +369,24 @@ final class WraparoundAdjustment {
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 {
+ if (inputToShiftable.isIdentity()) {
shifted = null;
lowerCorner = areaOfInterest.getLowerCorner();
upperCorner = areaOfInterest.getUpperCorner();
+ } else {
+ shifted = Envelopes.transform(inputToShiftable, areaOfInterest);
+ lowerCorner = shifted.getLowerCorner();
+ upperCorner = shifted.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 = WraparoundApplicator.range(cs, i);
+ for (int i=0; i<periods.length; i++) {
+ final double period = periods[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
@@ -268,8 +424,8 @@ final class WraparoundAdjustment {
* 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 validStart = shiftableDomain.getMinimum(i);
+ final double validEnd = shiftableDomain.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);
@@ -346,18 +502,28 @@ final class WraparoundAdjustment {
* at construction time.
*/
if (lowerCycles != 0 || upperCycles != 0) {
- isResultTransformed = true;
+ isResultShifted = true;
if (shifted == null) {
shifted = new GeneralEnvelope(areaOfInterest);
}
- areaOfInterest = shifted; // `shifted` may have been set before the loop.
+ 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);
}
}
}
}
- return Envelopes.transform(toFinal(), areaOfInterest);
+ /*
+ * Unconditionally apply the final transform (even if identity), unless
+ * `inputCRS` is null in which case the transform to apply is unknown.
+ */
+ final MathTransform toResult = toResult(isResultShifted);
+ if (toResult != null) {
+ final GeneralEnvelope result = Envelopes.transform(toResult, areaOfInterest);
+ result.setCoordinateReferenceSystem(resultCRS);
+ return result;
+ }
+ return GeneralEnvelope.castOrCopy(areaOfInterest);
}
/**
@@ -367,8 +533,8 @@ final class WraparoundAdjustment {
* 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>
+ * <p>The coordinate reference system should be specified in the {@code pointOfInterest}.
+ * If not, then the CRS is assumed same as the CRS of the domain specified at construction time.</p>
*
* @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;
@@ -377,24 +543,23 @@ final class WraparoundAdjustment {
* @throws TransformException if a coordinate conversion failed.
*/
public DirectPosition shift(DirectPosition pointOfInterest) throws TransformException {
- if (setIfNonNull(pointOfInterest.getCoordinateReferenceSystem())) {
+ boolean isResultShifted = false;
+ if (initialize(pointOfInterest.getCoordinateReferenceSystem())) {
DirectPosition shifted;
- if (replaceCRS()) {
- shifted = geographicToAOI.inverse().transform(pointOfInterest, null);
+ if (inputToShiftable.isIdentity()) {
+ shifted = pointOfInterest; // To be replaced by a copy of `pointOfInterest` when first needed.
} else {
- shifted = pointOfInterest; // To be replaced by a copy of `pointOfInterest` when first needed.
+ shifted = inputToShiftable.transform(pointOfInterest, null);
}
- final CoordinateSystem cs = crs.getCoordinateSystem();
- for (int i=cs.getDimension(); --i >= 0;) {
- final double period = WraparoundApplicator.range(cs, i);
+ for (int i=0; i<periods.length; i++) {
+ final double period = periods[i];
if (period > 0) {
- transformDomainToAOI();
final double x = shifted.getOrdinate(i);
- double delta = domainOfValidity.getMinimum(i) - x;
+ double delta = shiftableDomain.getMinimum(i) - x;
if (delta > 0) { // Test for point before domain of validity.
delta = Math.ceil(delta / period);
} else {
- delta = domainOfValidity.getMaximum(i) - x;
+ delta = shiftableDomain.getMaximum(i) - x;
if (delta < 0) { // Test for point after domain of validity.
delta = Math.floor(delta / period);
} else {
@@ -402,7 +567,7 @@ final class WraparoundAdjustment {
}
}
if (delta != 0) {
- isResultTransformed = true;
+ isResultShifted = true;
if (shifted == pointOfInterest) {
shifted = new GeneralDirectPosition(pointOfInterest);
}
@@ -412,6 +577,15 @@ final class WraparoundAdjustment {
}
}
}
- return toFinal().transform(pointOfInterest, null);
+ /*
+ * Unconditionally apply the final transform (even if identity), unless
+ * `inputCRS` is null in which case the transform to apply is unknown.
+ */
+ final MathTransform toResult = toResult(isResultShifted);
+ if (toResult != null) {
+ pointOfInterest = toResult.transform(pointOfInterest,
+ (resultCRS != null) ? new GeneralDirectPosition(resultCRS) : null);
+ }
+ return pointOfInterest;
}
}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/WraparoundAdjustmentTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/WraparoundAdjustmentTest.java
index c168222..8a89fb9 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/WraparoundAdjustmentTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/WraparoundAdjustmentTest.java
@@ -22,7 +22,6 @@ import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.crs.DefaultProjectedCRS;
import org.apache.sis.referencing.crs.HardCodedCRS;
import org.apache.sis.referencing.operation.HardCodedConversions;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.test.TestCase;
@@ -35,7 +34,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
* Tests {@link WraparoundAdjustment}.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
@@ -46,7 +45,7 @@ public final strictfp class WraparoundAdjustmentTest extends TestCase {
private static Envelope adjustWraparoundAxes(Envelope areaOfInterest, Envelope domainOfValidity, MathTransform validToAOI)
throws TransformException
{
- WraparoundAdjustment adj = new WraparoundAdjustment(domainOfValidity, validToAOI, MathTransforms.identity(2));
+ WraparoundAdjustment adj = new WraparoundAdjustment(domainOfValidity, validToAOI, null);
return adj.shift(areaOfInterest);
}