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 b38a9a6  Refactor the ReferencingUtilities.adjustWraparoundAxes(…) 
static method as a WraparoundAdjustment class. The intent is to make easier to 
improve it with handling of ProjectedCRS for now, maybe additional kinds of CRS 
in the future.
b38a9a6 is described below

commit b38a9a68d87216c517f260a4d49e9fea1159b9ea
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Mar 18 20:31:58 2019 +0100

    Refactor the ReferencingUtilities.adjustWraparoundAxes(…) static method as 
a WraparoundAdjustment class.
    The intent is to make easier to improve it with handling of ProjectedCRS 
for now, maybe additional kinds of CRS in the future.
---
 .../apache/sis/coverage/grid/GridDerivation.java   |   9 +-
 .../internal/referencing/ReferencingUtilities.java | 215 ---------------
 .../internal/referencing/WraparoundAdjustment.java | 297 +++++++++++++++++++++
 .../referencing/ReferencingUtilitiesTest.java      | 111 +-------
 .../referencing/WraparoundAdjustmentTest.java      | 152 +++++++++++
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 6 files changed, 456 insertions(+), 329 deletions(-)

diff --git 
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
 
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index bd037e5..67d5186 100644
--- 
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++ 
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -32,7 +32,7 @@ import 
org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.CRS;
-import org.apache.sis.internal.referencing.ReferencingUtilities;
+import org.apache.sis.internal.referencing.WraparoundAdjustment;
 import org.apache.sis.internal.referencing.DirectPositionView;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.GeneralEnvelope;
@@ -473,7 +473,7 @@ public class GridDerivation {
      *
      * @see GridExtent#subsample(int[])
      */
-    public GridDerivation subgrid(Envelope areaOfInterest, double... 
resolution) {
+    public GridDerivation subgrid(final Envelope areaOfInterest, double... 
resolution) {
         ensureSubgridNotSet();
         MathTransform cornerToCRS = base.requireGridToCRS();
         subGridSetter = "subgrid";
@@ -502,8 +502,9 @@ public class GridDerivation {
             dimension = baseExtent.getDimension();      // Non-null since 
'base.requireGridToCRS()' succeed.
             GeneralEnvelope indices = null;
             if (areaOfInterest != null) {
-                areaOfInterest = 
ReferencingUtilities.adjustWraparoundAxes(areaOfInterest, base.envelope, 
baseToAOI);
-                indices = Envelopes.transform(cornerToCRS.inverse(), 
areaOfInterest);
+                final WraparoundAdjustment adj = new 
WraparoundAdjustment(areaOfInterest);
+                adj.shiftInto(base.envelope, baseToAOI);
+                indices = adj.result(cornerToCRS.inverse());
                 clipExtent(indices);
             }
             if (indices == null || indices.getDimension() != dimension) {
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
index 8611537..a5441de 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
@@ -28,8 +28,6 @@ import org.opengis.metadata.citation.Citation;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.GeneralParameterDescriptor;
-import org.opengis.geometry.DirectPosition;
-import org.opengis.geometry.Envelope;
 import org.opengis.referencing.cs.*;
 import org.opengis.referencing.crs.*;
 import org.opengis.referencing.IdentifiedObject;
@@ -39,22 +37,13 @@ import org.opengis.referencing.datum.PrimeMeridian;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.VerticalDatum;
 import org.opengis.referencing.datum.VerticalDatumType;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
-import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.internal.system.DefaultFactories;
-import org.apache.sis.internal.metadata.AxisDirections;
-import org.apache.sis.measure.Longitude;
-import org.apache.sis.measure.Units;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.math.MathFunctions;
-import org.apache.sis.geometry.Envelopes;
-import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.datum.DefaultPrimeMeridian;
@@ -107,39 +96,6 @@ public final class ReferencingUtilities extends Static {
     }
 
     /**
-     * Returns the range (maximum - minimum) of the given axis if it has 
wraparound meaning,
-     * or {@link Double#NaN} otherwise. This method implements a fallback for 
the longitude
-     * axis if it does not declare the minimum and maximum values as expected.
-     *
-     * @param  cs         the coordinate system for which to get wraparound 
range, or {@code null}.
-     * @param  dimension  dimension of the axis to test.
-     * @return the wraparound range, or {@link Double#NaN} if none.
-     *
-     * @since 1.0
-     */
-    public static double getWraparoundRange(final CoordinateSystem cs, final 
int dimension) {
-        if (cs != null) {
-            final CoordinateSystemAxis axis = cs.getAxis(dimension);
-            if (axis != null && 
RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
-                double period = axis.getMaximumValue() - 
axis.getMinimumValue();
-                if (period > 0 && period != Double.POSITIVE_INFINITY) {
-                    return period;
-                }
-                final AxisDirection dir = 
AxisDirections.absolute(axis.getDirection());
-                if (AxisDirection.EAST.equals(dir) && cs instanceof 
EllipsoidalCS) {
-                    period = Longitude.MAX_VALUE - Longitude.MIN_VALUE;
-                    final Unit<?> unit = axis.getUnit();
-                    if (unit != null) {
-                        period = 
Units.DEGREE.getConverterTo(Units.ensureAngular(unit)).convert(period);
-                    }
-                    return period;
-                }
-            }
-        }
-        return Double.NaN;
-    }
-
-    /**
      * Returns the unit used for all axes in the given coordinate system.
      * If not all axes use the same unit, then this method returns {@code 
null}.
      *
@@ -532,175 +488,4 @@ public final class ReferencingUtilities extends Static {
         }
         return mapping;
     }
-
-    /**
-     * Returns an envelope with coordinates equivalent to the given 
coordinates,
-     * but potentially shifted for intersecting the given domain of validity.
-     * The dimensions that may be shifted are the ones having an axis with 
wraparound meaning.
-     *
-     * <p>The coordinate reference system must be specified in the given 
{@code areaOfInterest},
-     * or (as a fallback) in the given {@code domainOfValidity}. If none of 
those envelope have
-     * a CRS, then this method does nothing. If any envelope is null, then 
this method returns
-     * {@code areaOfInterest} unchanged.</p>
-     *
-     * <p>This method does not intersect the area of interest with the domain 
of validity.
-     * It is up to the caller to compute that intersection after this method 
call, if desired.</p>
-     *
-     * @param  areaOfInterest    the envelope to potentially shift toward the 
domain of validity, or {@code null} if none.
-     * @param  domainOfValidity  the domain of validity, or {@code null} if 
none.
-     * @param  validToAOI        if the envelopes do not use the same CRS, the 
transformation from {@code domainOfValidity}
-     *                           to {@code areaOfInterest}. Otherwise {@code 
null}. This method does not check by itself if
-     *                           a coordinate operation is needed; it must be 
supplied.
-     * @return the given area of interest, possibly shifted toward the domain 
of validity. May also be expanded.
-     * @throws TransformException if an envelope transformation was required 
but failed.
-     *
-     * @see GeneralEnvelope#simplify()
-     *
-     * @since 1.0
-     */
-    public static Envelope adjustWraparoundAxes(final Envelope areaOfInterest, 
Envelope domainOfValidity,
-            CoordinateOperation validToAOI) throws TransformException
-    {
-        CoordinateReferenceSystem crs;
-        if (areaOfInterest != null && domainOfValidity != null &&
-                ((crs =   areaOfInterest.getCoordinateReferenceSystem()) != 
null ||
-                 (crs = domainOfValidity.getCoordinateReferenceSystem()) != 
null))
-        {
-            GeneralEnvelope shifted = null;
-            final DirectPosition lowerCorner = areaOfInterest.getLowerCorner();
-            final DirectPosition upperCorner = areaOfInterest.getUpperCorner();
-            final CoordinateSystem cs = crs.getCoordinateSystem();
-            for (int i=cs.getDimension(); --i >= 0;) {
-                final double period = getWraparoundRange(cs, i);
-                if (period > 0) {
-                    /*
-                     * Found an axis (typically the longitude axis) with 
wraparound range meaning.
-                     * We are going to need the domain of validity in the same 
CRS than the AOI.
-                     * Transform that envelope when first needed.
-                     */
-                    if (validToAOI != null) {
-                        final MathTransform mt = validToAOI.getMathTransform();
-                        validToAOI = null;
-                        if (!mt.isIdentity()) {
-                            domainOfValidity = Envelopes.transform(mt, 
domainOfValidity);
-                        }
-                    }
-                    /*
-                     * "Unroll" the range. For example if we have [+160 … 
-170]° of longitude, we can replace by [160 … 190]°.
-                     * We do not change the 'lower' or 'upper' value now in 
order to avoid rounding error. Instead we compute
-                     * how many periods we need to add to those values. We 
adjust the side which results in the value closest
-                     * to zero, in order to reduce rounding error if no more 
adjustment is done in the next block.
-                     */
-                    final double lower = lowerCorner.getOrdinate(i);
-                    final double upper = upperCorner.getOrdinate(i);
-                    double lowerCycles = 0;                             // In 
number of periods.
-                    double upperCycles = 0;
-                    double delta = upper - lower;
-                    if (MathFunctions.isNegative(delta)) {              // Use 
'isNegative' for catching [+0 … -0] range.
-                        final double cycles = (delta == 0) ? -1 : 
Math.floor(delta / period);         // Always negative.
-                        delta = cycles * period;
-                        if (Math.abs(lower + delta) < Math.abs(upper - delta)) 
{
-                            lowerCycles = cycles;                              
      // Will subtract periods to 'lower'.
-                        } else {
-                            upperCycles = -cycles;                             
      // Will add periods to 'upper'.
-                        }
-                    }
-                    /*
-                     * The range may be before or after the domain of 
validity. Compute the distance from current
-                     * lower/upper coordinate to the coordinate of validity 
domain  (the sign tells us whether we
-                     * are before or after). The cases can be:
-                     *
-                     *   
┌─────────────┬────────────┬────────────────────────────┬───────────────────────────────┐
-                     *   │lowerIsBefore│upperIsAfter│ Meaning                  
  │ Action                        │
-                     *   
├─────────────┼────────────┼────────────────────────────┼───────────────────────────────┤
-                     *   │    false    │    false   │ AOI is inside valid area 
  │ Nothing to do                 │
-                     *   │    true     │    true    │ AOI encompasses valid 
area │ Nothing to do                 │
-                     *   │    true     │    false   │ AOI on left of valid 
area  │ Add positive amount of period │
-                     *   │    false    │    true    │ AOI on right of valid 
area │ Add negative amount of period │
-                     *   
└─────────────┴────────────┴────────────────────────────┴───────────────────────────────┘
-                     *
-                     * We try to compute multiples of 'periods' instead than 
just adding or subtracting 'periods' once in
-                     * order to support images that cover more than one 
period, for example images over 720° of longitude.
-                     * It may happen for example if an image shows data under 
the trajectory of a satellite.
-                     */
-                    final double  validStart        = 
domainOfValidity.getMinimum(i);
-                    final double  validEnd          = 
domainOfValidity.getMaximum(i);
-                    final double  lowerToValidStart = ((validStart - lower) / 
period) - lowerCycles;    // In number of periods.
-                    final double  upperToValidEnd   = ((validEnd   - upper) / 
period) - upperCycles;
-                    final boolean lowerIsBefore     = (lowerToValidStart > 0);
-                    final boolean upperIsAfter      = (upperToValidEnd < 0);
-                    if (lowerIsBefore != upperIsAfter) {
-                        final double upperToValidStart = ((validStart - upper) 
/ period) - upperCycles;
-                        final double lowerToValidEnd   = ((validEnd   - lower) 
/ period) - lowerCycles;
-                        if (lowerIsBefore) {
-                            /*
-                             * We need to add an integer amount of 'period' to 
both sides in order to move the range
-                             * inside the valid area. We need  
⎣lowerToValidStart⎦  for reaching the point where:
-                             *
-                             *     (validStart - period) < (new lower) ≦ 
validStart
-                             *
-                             * But we may add more because there will be no 
intersection without following condition:
-                             *
-                             *     (new upper) ≧ validStart
-                             *
-                             * That second condition is met by  
⎡upperToValidStart⎤. Note: ⎣x⎦=floor(x) and ⎡x⎤=ceil(x).
-                             */
-                            final double cycles = 
Math.max(Math.floor(lowerToValidStart), Math.ceil(upperToValidStart));
-                            /*
-                             * If after the shift we see that the following 
condition hold:
-                             *
-                             *     (new lower) + period < validEnd
-                             *
-                             * Then we may have a situation like below:
-                             *                  
┌────────────────────────────────────────────┐
-                             *                  │             Domain of 
validity             │
-                             *                  
└────────────────────────────────────────────┘
-                             *   ┌────────────────────┐                        
        ┌─────
-                             *   │  Area of interest  │                        
        │  AOI
-                             *   └────────────────────┘                        
        └─────
-                             *    
↖……………………………………………………………period……………………………………………………………↗︎
-                             *
-                             * The user may be requesting two extremums of the 
domain of validity. We can not express
-                             * that with a single envelope. Instead, we will 
expand the Area Of Interest to encompass
-                             * the full domain of validity.
-                             */
-                            if (cycles + 1 < lowerToValidEnd) {
-                                upperCycles += Math.ceil(upperToValidEnd);
-                            } else {
-                                upperCycles += cycles;
-                            }
-                            lowerCycles += cycles;
-                        } else {
-                            /*
-                             * Same reasoning than above with sign reverted 
and lower/upper variables interchanged.
-                             * In this block, 'upperToValidEnd' and 
'lowerToValidEnd' are negative, contrarily to
-                             * above block where they were positive.
-                             */
-                            final double cycles = 
Math.min(Math.ceil(upperToValidEnd), Math.floor(lowerToValidEnd));
-                            if (cycles - 1 > upperToValidStart) {
-                                lowerCycles += Math.floor(lowerToValidStart);
-                            } else {
-                                lowerCycles += cycles;
-                            }
-                            upperCycles += cycles;
-                        }
-                    }
-                    /*
-                     * If there is change to apply, copy the envelope when 
first needed.
-                     */
-                    if (lowerCycles != 0 || upperCycles != 0) {
-                        if (shifted == null) {
-                            shifted = new GeneralEnvelope(areaOfInterest);
-                        }
-                        shifted.setRange(i, lower + lowerCycles * period,      
 // TODO: use Math.fma in JDK9.
-                                            upper + upperCycles * period);
-                    }
-                }
-            }
-            if (shifted != null) {
-                return shifted;
-            }
-        }
-        return areaOfInterest;
-    }
 }
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
new file mode 100644
index 0000000..2641126
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAdjustment.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing;
+
+import javax.measure.Unit;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.cs.RangeMeaning;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.internal.metadata.AxisDirections;
+import org.apache.sis.measure.Longitude;
+import org.apache.sis.measure.Units;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+
+
+/**
+ * Adjustments applied on an envelope for handling wraparound axes. The 
adjustments consist in shifting
+ * some axes by an integer amount of periods, typically (not necessarily) 360° 
of longitude.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final class WraparoundAdjustment {
+    /**
+     * The envelope to potentially shift in order to fit in the domain of 
validity. If a shift is needed, then
+     * this envelope will be replaced by a new envelope; the user-specified 
envelope will not be modified.
+     */
+    private Envelope areaOfInterest;
+
+    /**
+     * If {@link #areaOfInterest} has been converted to a geographic CRS, the 
transformation back to its original CRS.
+     * Otherwise {@code null}.
+     */
+    private MathTransform geographicToAOI;
+
+    /**
+     * Creates a new instance for adjusting the given envelope.
+     * The given envelope will not be modified; a copy will be created if 
needed.
+     *
+     * @param  areaOfInterest  the envelope to potentially shift toward the 
domain of validity.
+     */
+    public WraparoundAdjustment(final Envelope areaOfInterest) {
+        this.areaOfInterest = areaOfInterest;
+    }
+
+    /**
+     * Returns the range (maximum - minimum) of the given axis if it has 
wraparound meaning,
+     * or {@link Double#NaN} otherwise. This method implements a fallback for 
the longitude
+     * axis if it does not declare the minimum and maximum values as expected.
+     *
+     * @param  cs         the coordinate system for which to get wraparound 
range, or {@code null}.
+     * @param  dimension  dimension of the axis to test.
+     * @return the wraparound range, or {@link Double#NaN} if none.
+     */
+    static double range(final CoordinateSystem cs, final int dimension) {
+        if (cs != null) {
+            final CoordinateSystemAxis axis = cs.getAxis(dimension);
+            if (axis != null && 
RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
+                double period = axis.getMaximumValue() - 
axis.getMinimumValue();
+                if (period > 0 && period != Double.POSITIVE_INFINITY) {
+                    return period;
+                }
+                final AxisDirection dir = 
AxisDirections.absolute(axis.getDirection());
+                if (AxisDirection.EAST.equals(dir) && cs instanceof 
EllipsoidalCS) {
+                    period = Longitude.MAX_VALUE - Longitude.MIN_VALUE;
+                    final Unit<?> unit = axis.getUnit();
+                    if (unit != null) {
+                        period = 
Units.DEGREE.getConverterTo(Units.ensureAngular(unit)).convert(period);
+                    }
+                    return period;
+                }
+            }
+        }
+        return Double.NaN;
+    }
+
+    /**
+     * Computes an envelope with coordinates equivalent to the {@code 
areaOfInterest} specified
+     * at construction time, but potentially shifted for intersecting the 
given domain of validity.
+     * The dimensions that may be shifted are the ones having an axis with 
wraparound meaning.
+     * The envelope may have been converted to a geographic CRS for performing 
this operation.
+     *
+     * <p>The coordinate reference system must be specified in the {@code 
areaOfInterest}
+     * specified at construction time, or (as a fallback) in the given {@code 
domainOfValidity}.
+     * If none of those envelopes have a CRS, then this method does 
nothing.</p>
+     *
+     * <p>This method does not intersect the area of interest with the domain 
of validity.
+     * It is up to the caller to compute that intersection after this method 
call, if desired.</p>
+     *
+     * @param  domainOfValidity  the domain of validity, or {@code null} if 
none.
+     * @param  validToAOI        if the envelopes do not use the same CRS, the 
transformation from {@code domainOfValidity}
+     *                           to {@code areaOfInterest}. Otherwise {@code 
null}. This method does not check by itself if
+     *                           a coordinate operation is needed; it must be 
supplied.
+     * @throws TransformException if an envelope transformation was required 
but failed.
+     *
+     * @see GeneralEnvelope#simplify()
+     */
+    public void shiftInto(Envelope domainOfValidity, CoordinateOperation 
validToAOI) throws TransformException {
+        CoordinateReferenceSystem crs = 
areaOfInterest.getCoordinateReferenceSystem();
+        if (crs == null) {
+            crs = domainOfValidity.getCoordinateReferenceSystem();      // 
Assumed to apply to AOI too.
+            if (crs == null) {
+                return;
+            }
+        }
+        /*
+         * If the coordinate reference system is a projected CRS, it will not 
have any wraparound axis.
+         * We need to perform the verification in its base geographic CRS 
instead, and remember that we
+         * may need to transform the result later.
+         */
+        GeneralEnvelope shifted = null;         // To be initialized to a copy 
of 'areaOfInterest' when first needed.
+        if (crs instanceof ProjectedCRS) {
+            final ProjectedCRS p = (ProjectedCRS) crs;
+            crs = p.getBaseCRS();
+            geographicToAOI = p.getConversionFromBase().getMathTransform();
+            areaOfInterest = shifted = 
Envelopes.transform(geographicToAOI.inverse(), areaOfInterest);
+        }
+        /*
+         * We will not reference 'areaOfInterest' anymore after we got its two 
corner points.
+         * The following loop search for "wraparound" axis.
+         */
+        final DirectPosition lowerCorner = areaOfInterest.getLowerCorner();
+        final DirectPosition upperCorner = areaOfInterest.getUpperCorner();
+        final CoordinateSystem cs = crs.getCoordinateSystem();
+        for (int i=cs.getDimension(); --i >= 0;) {
+            final double period = range(cs, i);
+            if (period > 0) {
+                /*
+                 * Found an axis (typically the longitude axis) with 
wraparound range meaning.
+                 * We are going to need the domain of validity in the same CRS 
than the AOI.
+                 * Transform that envelope when first needed.
+                 */
+                if (validToAOI != null) {
+                    MathTransform mt = validToAOI.getMathTransform();
+                    if (geographicToAOI != null) {
+                        mt = MathTransforms.concatenate(mt, 
geographicToAOI.inverse());
+                    }
+                    validToAOI = null;
+                    if (!mt.isIdentity()) {
+                        domainOfValidity = Envelopes.transform(mt, 
domainOfValidity);
+                    }
+                }
+                /*
+                 * "Unroll" the range. For example if we have [+160 … -170]° 
of longitude, we can replace by [160 … 190]°.
+                 * We do not change the 'lower' or 'upper' value now in order 
to avoid rounding error. Instead we compute
+                 * how many periods we need to add to those values. We adjust 
the side which results in the value closest
+                 * to zero, in order to reduce rounding error if no more 
adjustment is done in the next block.
+                 */
+                final double lower = lowerCorner.getOrdinate(i);
+                final double upper = upperCorner.getOrdinate(i);
+                double lowerCycles = 0;                             // In 
number of periods.
+                double upperCycles = 0;
+                double delta = upper - lower;
+                if (MathFunctions.isNegative(delta)) {              // Use 
'isNegative' for catching [+0 … -0] range.
+                    final double cycles = (delta == 0) ? -1 : Math.floor(delta 
/ period);         // Always negative.
+                    delta = cycles * period;
+                    if (Math.abs(lower + delta) < Math.abs(upper - delta)) {
+                        lowerCycles = cycles;                                  
  // Will subtract periods to 'lower'.
+                    } else {
+                        upperCycles = -cycles;                                 
  // Will add periods to 'upper'.
+                    }
+                }
+                /*
+                 * The range may be before or after the domain of validity. 
Compute the distance from current
+                 * lower/upper coordinate to the coordinate of validity domain 
 (the sign tells us whether we
+                 * are before or after). The cases can be:
+                 *
+                 *   
┌─────────────┬────────────┬────────────────────────────┬───────────────────────────────┐
+                 *   │lowerIsBefore│upperIsAfter│ Meaning                    │ 
Action                        │
+                 *   
├─────────────┼────────────┼────────────────────────────┼───────────────────────────────┤
+                 *   │    false    │    false   │ AOI is inside valid area   │ 
Nothing to do                 │
+                 *   │    true     │    true    │ AOI encompasses valid area │ 
Nothing to do                 │
+                 *   │    true     │    false   │ AOI on left of valid area  │ 
Add positive amount of period │
+                 *   │    false    │    true    │ AOI on right of valid area │ 
Add negative amount of period │
+                 *   
└─────────────┴────────────┴────────────────────────────┴───────────────────────────────┘
+                 *
+                 * We try to compute multiples of 'periods' instead than just 
adding or subtracting 'periods' once in
+                 * order to support images that cover more than one period, 
for example images over 720° of longitude.
+                 * It may happen for example if an image shows data under the 
trajectory of a satellite.
+                 */
+                final double  validStart        = 
domainOfValidity.getMinimum(i);
+                final double  validEnd          = 
domainOfValidity.getMaximum(i);
+                final double  lowerToValidStart = ((validStart - lower) / 
period) - lowerCycles;    // In number of periods.
+                final double  upperToValidEnd   = ((validEnd   - upper) / 
period) - upperCycles;
+                final boolean lowerIsBefore     = (lowerToValidStart > 0);
+                final boolean upperIsAfter      = (upperToValidEnd < 0);
+                if (lowerIsBefore != upperIsAfter) {
+                    final double upperToValidStart = ((validStart - upper) / 
period) - upperCycles;
+                    final double lowerToValidEnd   = ((validEnd   - lower) / 
period) - lowerCycles;
+                    if (lowerIsBefore) {
+                        /*
+                         * We need to add an integer amount of 'period' to 
both sides in order to move the range
+                         * inside the valid area. We need  ⎣lowerToValidStart⎦ 
 for reaching the point where:
+                         *
+                         *     (validStart - period) < (new lower) ≦ validStart
+                         *
+                         * But we may add more because there will be no 
intersection without following condition:
+                         *
+                         *     (new upper) ≧ validStart
+                         *
+                         * That second condition is met by  
⎡upperToValidStart⎤. Note: ⎣x⎦=floor(x) and ⎡x⎤=ceil(x).
+                         */
+                        final double cycles = 
Math.max(Math.floor(lowerToValidStart), Math.ceil(upperToValidStart));
+                        /*
+                         * If after the shift we see that the following 
condition hold:
+                         *
+                         *     (new lower) + period < validEnd
+                         *
+                         * Then we may have a situation like below:
+                         *                  
┌────────────────────────────────────────────┐
+                         *                  │             Domain of validity   
          │
+                         *                  
└────────────────────────────────────────────┘
+                         *   ┌────────────────────┐                            
    ┌─────
+                         *   │  Area of interest  │                            
    │  AOI
+                         *   └────────────────────┘                            
    └─────
+                         *    
↖……………………………………………………………period……………………………………………………………↗︎
+                         *
+                         * The user may be requesting two extremums of the 
domain of validity. We can not express
+                         * that with a single envelope. Instead, we will 
expand the Area Of Interest to encompass
+                         * the full domain of validity.
+                         */
+                        if (cycles + 1 < lowerToValidEnd) {
+                            upperCycles += Math.ceil(upperToValidEnd);
+                        } else {
+                            upperCycles += cycles;
+                        }
+                        lowerCycles += cycles;
+                    } else {
+                        /*
+                         * Same reasoning than above with sign reverted and 
lower/upper variables interchanged.
+                         * In this block, 'upperToValidEnd' and 
'lowerToValidEnd' are negative, contrarily to
+                         * above block where they were positive.
+                         */
+                        final double cycles = 
Math.min(Math.ceil(upperToValidEnd), Math.floor(lowerToValidEnd));
+                        if (cycles - 1 > upperToValidStart) {
+                            lowerCycles += Math.floor(lowerToValidStart);
+                        } else {
+                            lowerCycles += cycles;
+                        }
+                        upperCycles += cycles;
+                    }
+                }
+                /*
+                 * If there is change to apply, copy the envelope when first 
needed.
+                 */
+                if (lowerCycles != 0 || upperCycles != 0) {
+                    if (shifted == null) {
+                        areaOfInterest = shifted = new 
GeneralEnvelope(areaOfInterest);
+                    }
+                    shifted.setRange(i, lower + lowerCycles * period,       // 
TODO: use Math.fma in JDK9.
+                                        upper + upperCycles * period);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the (potentially shifted and/or expanded) area of interest 
converted by the given transform.
+     *
+     * @param  mt  a transform from the CRS of the {@code areaOfInterest} 
given to the constructor.
+     * @return the area of interest transformed by the given {@code 
MathTransform}.
+     * @throws TransformException if the transformation failed.
+     */
+    public GeneralEnvelope result(MathTransform mt) throws TransformException {
+        if (geographicToAOI != null) {
+            mt = MathTransforms.concatenate(geographicToAOI, mt);
+        }
+        return Envelopes.transform(mt, areaOfInterest);
+    }
+}
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
index 207c714..a9b1143 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
@@ -17,25 +17,20 @@
 package org.apache.sis.internal.referencing;
 
 import javax.measure.Unit;
-import org.apache.sis.geometry.GeneralEnvelope;
-import org.opengis.geometry.Envelope;
 import org.opengis.referencing.cs.*;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.datum.PrimeMeridian;
 import org.opengis.referencing.datum.VerticalDatum;
 import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.operation.CoordinateOperation;
-import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.datum.HardCodedDatum;
 import org.apache.sis.referencing.crs.HardCodedCRS;
-import org.apache.sis.referencing.cs.HardCodedCS;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.measure.Units;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.apache.sis.test.ReferencingAssert.*;
+import static org.junit.Assert.*;
 import static org.apache.sis.internal.referencing.ReferencingUtilities.*;
 
 
@@ -62,16 +57,6 @@ public final strictfp class ReferencingUtilitiesTest extends 
TestCase {
     }
 
     /**
-     * Tests {@link ReferencingUtilities#getWraparoundRange(CoordinateSystem, 
int)}.
-     */
-    @Test
-    public void testGetWraparoundRange() {
-        assertTrue  (Double.isNaN(getWraparoundRange(HardCodedCS.GEODETIC_φλ, 
0)));
-        assertEquals(360, getWraparoundRange(HardCodedCS.GEODETIC_φλ, 1), 
STRICT);
-        assertEquals(400, getWraparoundRange(HardCodedCS.ELLIPSOIDAL_gon, 0), 
STRICT);
-    }
-
-    /**
      * Tests {@link ReferencingUtilities#isEllipsoidalHeight(VerticalDatum)}.
      */
     @Test
@@ -140,98 +125,4 @@ public final strictfp class ReferencingUtilitiesTest 
extends TestCase {
         assertEquals("timeCS",           
toPropertyName(CoordinateSystem.class, TimeCS          .class).toString());
         assertEquals("verticalCS",       
toPropertyName(CoordinateSystem.class, VerticalCS      .class).toString());
     }
-
-    /**
-     * Tests {@link ReferencingUtilities#adjustWraparoundAxes(Envelope, 
Envelope, CoordinateOperation)}
-     * with an envelope crossing the anti-meridian.
-     *
-     * @throws TransformException should never happen since this test does not 
transform coordinates.
-     *
-     * @since 1.0
-     */
-    @Test
-    public void testAdjustWraparoundAxesOverAntiMeridian() throws 
TransformException {
-        final GeneralEnvelope domainOfValidity = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        domainOfValidity.setRange(0,  80, 280);
-        domainOfValidity.setRange(1, -90, +90);
-
-        final GeneralEnvelope areaOfInterest = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        areaOfInterest.setRange(0, 140, -179);                 // Cross 
anti-meridian.
-        areaOfInterest.setRange(1, -90,   90);
-
-        final GeneralEnvelope expected = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        expected.setRange(0, 140, 181);
-        expected.setRange(1, -90, +90);
-
-        final Envelope actual = 
ReferencingUtilities.adjustWraparoundAxes(areaOfInterest, domainOfValidity, 
null);
-        assertEnvelopeEquals(expected, actual);
-    }
-
-    /**
-     * Tests {@link ReferencingUtilities#adjustWraparoundAxes(Envelope, 
Envelope, CoordinateOperation)}
-     * with an envelope shifted by 360° before or after the grid valid area.
-     *
-     * @throws TransformException should never happen since this test does not 
transform coordinates.
-     *
-     * @since 1.0
-     */
-    @Test
-    public void testAdjustWraparoundAxesWithShiftedAOI() throws 
TransformException {
-        final GeneralEnvelope domainOfValidity = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        domainOfValidity.setRange(0,  80, 100);
-        domainOfValidity.setRange(1, -70, +70);
-
-        final GeneralEnvelope areaOfInterest = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        areaOfInterest.setRange(0,  70, 90);
-        areaOfInterest.setRange(1, -80, 60);
-
-        final GeneralEnvelope expected = new GeneralEnvelope(areaOfInterest);
-
-        Envelope actual = 
ReferencingUtilities.adjustWraparoundAxes(areaOfInterest, domainOfValidity, 
null);
-        assertEnvelopeEquals(expected, actual);
-
-        areaOfInterest.setRange(0, -290, -270);                    // [70 … 
90] - 360
-        actual = ReferencingUtilities.adjustWraparoundAxes(areaOfInterest, 
domainOfValidity, null);
-        assertEnvelopeEquals(expected, actual);
-
-        areaOfInterest.setRange(0, 430, 450);                      // [70 … 
90] + 360
-        actual = ReferencingUtilities.adjustWraparoundAxes(areaOfInterest, 
domainOfValidity, null);
-        assertEnvelopeEquals(expected, actual);
-    }
-
-    /**
-     * Tests {@link ReferencingUtilities#adjustWraparoundAxes(Envelope, 
Envelope, CoordinateOperation)}
-     * with an envelope that cause the method to expand the area of interest. 
Illustration:
-     *
-     * {@preformat text
-     *                  ┌────────────────────────────────────────────┐
-     *                  │             Domain of validity             │
-     *                  └────────────────────────────────────────────┘
-     *   ┌────────────────────┐                                ┌─────
-     *   │  Area of interest  │                                │  AOI
-     *   └────────────────────┘                                └─────
-     *    ↖………………………………………………………360° period……………………………………………………↗︎
-     * }
-     *
-     * @throws TransformException should never happen since this test does not 
transform coordinates.
-     *
-     * @since 1.0
-     */
-    @Test
-    public void testAdjustWraparoundAxesCausingExpansion() throws 
TransformException {
-        final GeneralEnvelope domainOfValidity = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        domainOfValidity.setRange(0,   5, 345);
-        domainOfValidity.setRange(1, -70, +70);
-
-        final GeneralEnvelope areaOfInterest = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        areaOfInterest.setRange(0, -30,  40);
-        areaOfInterest.setRange(1, -60,  60);
-
-        final GeneralEnvelope expected = new 
GeneralEnvelope(HardCodedCRS.WGS84);
-        expected.setRange(0, -30, 400);
-        expected.setRange(1, -60,  60);
-
-        final Envelope actual = 
ReferencingUtilities.adjustWraparoundAxes(areaOfInterest, domainOfValidity, 
null);
-        assertEnvelopeEquals(expected, actual);
-    }
 }
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
new file mode 100644
index 0000000..e6634ed
--- /dev/null
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing;
+
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.cs.*;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.crs.HardCodedCRS;
+import org.apache.sis.referencing.cs.HardCodedCS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.apache.sis.test.ReferencingAssert.*;
+
+
+/**
+ * Tests {@link WraparoundAdjustment}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class WraparoundAdjustmentTest extends TestCase {
+    /**
+     * Tests {@link WraparoundAdjustment#range(CoordinateSystem, int)}.
+     */
+    @Test
+    public void testRange() {
+        assertTrue  
(Double.isNaN(WraparoundAdjustment.range(HardCodedCS.GEODETIC_φλ, 0)));
+        assertEquals(360, WraparoundAdjustment.range(HardCodedCS.GEODETIC_φλ, 
1), STRICT);
+        assertEquals(400, 
WraparoundAdjustment.range(HardCodedCS.ELLIPSOIDAL_gon, 0), STRICT);
+    }
+
+    /**
+     * Convenience method for the tests.
+     */
+    private static Envelope adjustWraparoundAxes(Envelope areaOfInterest, 
Envelope domainOfValidity, CoordinateOperation validToAOI)
+            throws TransformException
+    {
+        WraparoundAdjustment adj = new WraparoundAdjustment(areaOfInterest);
+        adj.shiftInto(domainOfValidity, validToAOI);
+        return adj.result(MathTransforms.identity(2));
+    }
+
+    /**
+     * Tests {@link WraparoundAdjustment#shiftInto(Envelope, 
CoordinateOperation)}
+     * with an envelope crossing the anti-meridian.
+     *
+     * @throws TransformException should never happen since this test does not 
transform coordinates.
+     */
+    @Test
+    public void testOverAntiMeridian() throws TransformException {
+        final GeneralEnvelope domainOfValidity = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        domainOfValidity.setRange(0,  80, 280);
+        domainOfValidity.setRange(1, -90, +90);
+
+        final GeneralEnvelope areaOfInterest = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        areaOfInterest.setRange(0, 140, -179);                 // Cross 
anti-meridian.
+        areaOfInterest.setRange(1, -90,   90);
+
+        final GeneralEnvelope expected = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        expected.setRange(0, 140, 181);
+        expected.setRange(1, -90, +90);
+
+        final Envelope actual = adjustWraparoundAxes(areaOfInterest, 
domainOfValidity, null);
+        assertEnvelopeEquals(expected, actual);
+    }
+
+    /**
+     * Tests {@link WraparoundAdjustment#shiftInto(Envelope, 
CoordinateOperation)}
+     * with an envelope shifted by 360° before or after the grid valid area.
+     *
+     * @throws TransformException should never happen since this test does not 
transform coordinates.
+     */
+    @Test
+    @DependsOnMethod("testRange")
+    public void testWithShiftedAOI() throws TransformException {
+        final GeneralEnvelope domainOfValidity = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        domainOfValidity.setRange(0,  80, 100);
+        domainOfValidity.setRange(1, -70, +70);
+
+        final GeneralEnvelope areaOfInterest = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        areaOfInterest.setRange(0,  70, 90);
+        areaOfInterest.setRange(1, -80, 60);
+
+        final GeneralEnvelope expected = new GeneralEnvelope(areaOfInterest);
+
+        Envelope actual = adjustWraparoundAxes(areaOfInterest, 
domainOfValidity, null);
+        assertEnvelopeEquals(expected, actual);
+
+        areaOfInterest.setRange(0, -290, -270);                    // [70 … 
90] - 360
+        actual = adjustWraparoundAxes(areaOfInterest, domainOfValidity, null);
+        assertEnvelopeEquals(expected, actual);
+
+        areaOfInterest.setRange(0, 430, 450);                      // [70 … 
90] + 360
+        actual = adjustWraparoundAxes(areaOfInterest, domainOfValidity, null);
+        assertEnvelopeEquals(expected, actual);
+    }
+
+    /**
+     * Tests {@link WraparoundAdjustment#shiftInto(Envelope, 
CoordinateOperation)}
+     * with an envelope that cause the method to expand the area of interest. 
Illustration:
+     *
+     * {@preformat text
+     *                  ┌────────────────────────────────────────────┐
+     *                  │             Domain of validity             │
+     *                  └────────────────────────────────────────────┘
+     *   ┌────────────────────┐                                ┌─────
+     *   │  Area of interest  │                                │  AOI
+     *   └────────────────────┘                                └─────
+     *    ↖………………………………………………………360° period……………………………………………………↗︎
+     * }
+     *
+     * @throws TransformException should never happen since this test does not 
transform coordinates.
+     */
+    @Test
+    public void testAxesCausingExpansion() throws TransformException {
+        final GeneralEnvelope domainOfValidity = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        domainOfValidity.setRange(0,   5, 345);
+        domainOfValidity.setRange(1, -70, +70);
+
+        final GeneralEnvelope areaOfInterest = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        areaOfInterest.setRange(0, -30,  40);
+        areaOfInterest.setRange(1, -60,  60);
+
+        final GeneralEnvelope expected = new 
GeneralEnvelope(HardCodedCRS.WGS84);
+        expected.setRange(0, -30, 400);
+        expected.setRange(1, -60,  60);
+
+        final Envelope actual = adjustWraparoundAxes(areaOfInterest, 
domainOfValidity, null);
+        assertEnvelopeEquals(expected, actual);
+    }
+}
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index 8cd71a7..5494363 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -36,6 +36,7 @@ import org.junit.BeforeClass;
     org.apache.sis.internal.referencing.j2d.ShapeUtilitiesTest.class,
     org.apache.sis.internal.referencing.PositionalAccuracyConstantTest.class,
     org.apache.sis.internal.referencing.ReferencingUtilitiesTest.class,
+    org.apache.sis.internal.referencing.WraparoundAdjustmentTest.class,
     org.apache.sis.internal.referencing.WKTUtilitiesTest.class,
     org.apache.sis.internal.jaxb.referencing.CodeTest.class,
     org.apache.sis.internal.jaxb.referencing.SecondDefiningParameterTest.class,

Reply via email to