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 <martin.desruisse...@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);
     }
 

Reply via email to