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);
     }