sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: 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.
Date Mon, 18 Mar 2019 19:33:46 GMT
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 <martin.desruisseaux@geomatys.com>
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,


Mime
View raw message