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

commit fa0971363889cfea057aae4ae258273b56d3da24
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Jul 30 11:31:54 2022 +0200

    Partially revert commit 3fa0138 ("Apply a longitude wraparound on the 
Mercator projection").
    Instead, the Mercator projection applies no wraparound by default (same 
behavior as SIS 1.2)
    and the wraparound can be requested by invoking `createWithWraparound(…)`.
    
    https://issues.apache.org/jira/browse/SIS-547
    https://issues.apache.org/jira/browse/SIS-486
---
 .../operation/projection/AlbersEqualArea.java      |  31 +-
 .../operation/projection/AuthalicMercator.java     |  19 +-
 .../operation/projection/Initializer.java          |  37 +--
 .../projection/LambertConicConformal.java          |  27 +-
 .../operation/projection/LongitudeWraparound.java  | 347 +++++++++++++++++++++
 .../referencing/operation/projection/Mercator.java |  90 +++---
 .../operation/projection/Mollweide.java            |   6 +-
 .../operation/projection/NormalizedProjection.java | 114 +++++--
 .../operation/projection/ObliqueStereographic.java |  39 +--
 .../operation/projection/SatelliteTracking.java    |  40 ++-
 .../operation/projection/TransverseMercator.java   |   2 +-
 .../operation/projection/ZonedGridSystem.java      |   2 +-
 .../projection/LambertConicConformalTest.java      |   2 +-
 .../operation/projection/MercatorTest.java         |   8 +-
 .../projection/PolarStereographicTest.java         |   2 +-
 .../projection/TransverseMercatorTest.java         |   2 +-
 16 files changed, 553 insertions(+), 215 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
index fc1e246ffb..3267480e3d 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
@@ -50,7 +50,7 @@ import static 
org.apache.sis.internal.referencing.provider.AlbersEqualArea.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -58,7 +58,7 @@ public class AlbersEqualArea extends AuthalicConversion {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -3466040922402982480L;
+    private static final long serialVersionUID = -3024658742514888646L;
 
     /**
      * Internal coefficients for computation, depending only on eccentricity 
and values of standards parallels.
@@ -81,15 +81,6 @@ public class AlbersEqualArea extends AuthalicConversion {
      */
     final double C;
 
-    /**
-     * A bound of the [−n⋅π … n⋅π] range, which is the valid range of  θ = n⋅λ 
 values.
-     * Some (not all) θ values need to be shifted inside that range before to 
use them
-     * in trigonometric functions.
-     *
-     * @see Initializer#boundOfScaledLongitude(DoubleDouble)
-     */
-    final double θ_bound;
-
     /**
      * Creates an Albers Equal Area projection from the given parameters.
      *
@@ -173,7 +164,6 @@ public class AlbersEqualArea extends AuthalicConversion {
         denormalize.convertBefore(0, rn, null); rn.negate();
         denormalize.convertBefore(1, rn, ρ0);   rn.inverseDivide(-1);
         normalize.convertAfter(0, rn, null);    // On this line, `rn` became 
`n`.
-        θ_bound = initializer.boundOfScaledLongitude(rn);
     }
 
     /**
@@ -181,9 +171,8 @@ public class AlbersEqualArea extends AuthalicConversion {
      */
     AlbersEqualArea(final AlbersEqualArea other) {
         super(other);
-        nm      = other.nm;
-        C       = other.C;
-        θ_bound = other.θ_bound;
+        nm = other.nm;
+        C  = other.C;
     }
 
     /**
@@ -222,7 +211,7 @@ public class AlbersEqualArea extends AuthalicConversion {
         if (eccentricity == 0 && getClass() == AlbersEqualArea.class) {
             kernel = new Spherical(this);
         }
-        return context.completeTransform(factory, kernel);
+        return kernel.completeWithWraparound(factory);
     }
 
     /**
@@ -240,9 +229,8 @@ public class AlbersEqualArea extends AuthalicConversion {
                             final double[] dstPts, final int dstOff,
                             final boolean derivate) throws ProjectionException
     {
-        // θ = n⋅λ  reduced to  [−n⋅π … n⋅π]  range.
-        final double θ = wraparoundScaledLongitude(srcPts[srcOff], θ_bound);
-        final double φ = srcPts[srcOff + 1];
+        final double θ = srcPts[srcOff  ];      // θ = n⋅λ  reduced to  [−n⋅π 
… n⋅π]  range.
+        final double φ = srcPts[srcOff+1];
         final double cosθ = cos(θ);
         final double sinθ = sin(θ);
         final double sinφ = sin(φ);
@@ -329,9 +317,8 @@ public class AlbersEqualArea extends AuthalicConversion {
                                 final double[] dstPts, final int dstOff,
                                 final boolean derivate)
         {
-            // θ = n⋅λ  reduced to  [−n⋅π … n⋅π]  range.
-            final double θ = wraparoundScaledLongitude(srcPts[srcOff], 
θ_bound);
-            final double φ = srcPts[srcOff + 1];
+            final double θ = srcPts[srcOff  ];              // θ = n⋅λ  
reduced to  [−n⋅π … n⋅π]  range.
+            final double φ = srcPts[srcOff+1];
             final double cosθ = cos(θ);
             final double sinθ = sin(θ);
             final double sinφ = sin(φ);
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
index 656c64a4c2..7eff535735 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
@@ -16,9 +16,13 @@
  */
 package org.apache.sis.referencing.operation.projection;
 
+import java.util.Optional;
+import org.opengis.geometry.Envelope;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
+import org.apache.sis.referencing.operation.transform.DomainDefinition;
+import org.apache.sis.geometry.Envelope2D;
 
 import static java.lang.Math.*;
 import static org.apache.sis.math.MathFunctions.atanh;
@@ -29,7 +33,7 @@ import static org.apache.sis.math.MathFunctions.atanh;
  * This is used for implementation of ESRI "Mercator Auxiliary Sphere type 3" 
projection.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -53,6 +57,19 @@ final class AuthalicMercator extends AuthalicConversion {
         super(null, other);
     }
 
+    /**
+     * Returns the domain of input coordinates.
+     * This method is defined for consistency with {@link 
Mercator#getDomain(DomainDefinition)}.
+     *
+     * @since 1.3
+     */
+    @Override
+    public Optional<Envelope> getDomain(final DomainDefinition criteria) {
+        return Optional.of(new Envelope2D(null,
+                 -LARGE_LONGITUDE_LIMIT,  -POLAR_AREA_LIMIT,
+                2*LARGE_LONGITUDE_LIMIT, 2*POLAR_AREA_LIMIT));
+    }
+
     /**
      * Projects the specified (λ,φ) coordinates (units in radians) and stores 
the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if 
{@code derivate} is {@code true}.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
index b3551e0da6..a8107cc6bd 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
@@ -56,7 +56,7 @@ import static 
org.apache.sis.util.ArgumentChecks.ensureNonNull;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -384,39 +384,4 @@ final class Initializer {
         s.inverseDivide(cosφ);
         return s.doubleValue();
     }
-
-    /**
-     * Returns a bound of the [−n⋅π … n⋅π] range, which is the valid range of  
θ = n⋅λ  values.
-     * This method is invoked by map projections that multiply the longitude 
values by some scale factor before
-     * to use them in trigonometric functions. Usually we do not explicitly 
wraparound the longitude values,
-     * because trigonometric functions do that automatically for us. However 
if the longitude is multiplied
-     * by some factor before to be used in trigonometric functions, then that 
implicit wraparound is not the
-     * one we expect. The map projection code needs to perform explicit 
wraparound in such cases.
-     *
-     * @param  n  the factor by which longitude values are multiplied before 
use in trigonometry.
-     * @return a bound of the [−n⋅π … n⋅π] range.
-     *
-     * @see NormalizedProjection#wraparoundScaledLongitude(double, double)
-     * @see <a href="https://issues.apache.org/jira/browse/SIS-486";>SIS-486</a>
-     */
-    final double boundOfScaledLongitude(final double n) {
-        return boundOfScaledLongitude(new DoubleDouble(n));
-    }
-
-    /**
-     * Same as {@link #boundOfScaledLongitude(double)} with opportunistic use 
of double-double precision.
-     * This is used when than object is available anyway.
-     *
-     * @param  n  the factor by which longitude values are multiplied before 
use in trigonometry.
-     * @return a bound of the [−n⋅π … n⋅π] range.
-     */
-    final double boundOfScaledLongitude(final DoubleDouble n) {
-        if (signum_λ0 == 0 || n.doubleValue() >= 1) {
-            return Double.NaN;                          // Do not apply any 
wraparound.
-        }
-        final DoubleDouble r = DoubleDouble.createPi();
-        r.multiply(n);
-        final double θ_bound = abs(r.doubleValue());
-        return (signum_λ0 < 0) ? θ_bound : -θ_bound;
-    }
 }
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
index 911a4dd001..581e84ea01 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
@@ -74,7 +74,7 @@ public class LambertConicConformal extends 
ConformalProjection {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -8786460743797422415L;
+    private static final long serialVersionUID = 2067358524298002016L;
 
     /**
      * Variants of Lambert Conical Conformal projection. Those variants modify 
the way the projections are constructed
@@ -165,15 +165,6 @@ public class LambertConicConformal extends 
ConformalProjection {
      */
     final double n;
 
-    /**
-     * A bound of the [−n⋅π … n⋅π] range, which is the valid range of  θ = n⋅λ 
 values.
-     * Some (not all) θ values need to be shifted inside that range before to 
use them
-     * in trigonometric functions.
-     *
-     * @see Initializer#boundOfScaledLongitude(DoubleDouble)
-     */
-    final double θ_bound;
-
     /**
      * Creates a Lambert projection from the given parameters.
      * The {@code method} argument can be the description of one of the 
following:
@@ -375,7 +366,6 @@ public class LambertConicConformal extends 
ConformalProjection {
         normalize  .convertAfter(1, sφ, null);
         denormalize.convertBefore(0, F, null); F.negate();
         denormalize.convertBefore(1, F, rF);
-        θ_bound = initializer.boundOfScaledLongitude(sλ);
     }
 
     /**
@@ -383,8 +373,7 @@ public class LambertConicConformal extends 
ConformalProjection {
      */
     LambertConicConformal(final LambertConicConformal other) {
         super(other);
-        n       = other.n;
-        θ_bound = other.θ_bound;
+        n = other.n;
     }
 
     /**
@@ -423,7 +412,7 @@ public class LambertConicConformal extends 
ConformalProjection {
         if (eccentricity == 0 && getClass() == LambertConicConformal.class) {
             kernel = new Spherical(this);
         }
-        return context.completeTransform(factory, kernel);
+        return kernel.completeWithWraparound(factory);
     }
 
     /**
@@ -436,7 +425,7 @@ public class LambertConicConformal extends 
ConformalProjection {
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        final double x = abs(θ_bound);
+        final double x = getWraparoundLongitude();
         final double y = copySign(PI/2, -n);
         return Optional.of(new Envelope2D(null, -x, Math.min(y, 0), 2*x, 
Math.abs(y)));
     }
@@ -461,8 +450,8 @@ public class LambertConicConformal extends 
ConformalProjection {
          * the first non-linear one moved to the "normalize" affine transform, 
and the linear operations
          * applied after the last non-linear one moved to the "denormalize" 
affine transform.
          */
-        final double θ    = wraparoundScaledLongitude(srcPts[srcOff], 
θ_bound);     // θ = Δλ⋅n
-        final double φ    = srcPts[srcOff + 1];                         // 
Sign may be reversed
+        final double θ    = srcPts[srcOff  ];           // θ = Δλ⋅n
+        final double φ    = srcPts[srcOff+1];           // Sign may be reversed
         final double absφ = abs(φ);
         final double sinθ = sin(θ);
         final double cosθ = cos(θ);
@@ -572,8 +561,8 @@ public class LambertConicConformal extends 
ConformalProjection {
                                 final double[] dstPts, final int dstOff,
                                 final boolean derivate)
         {
-            final double θ    = wraparoundScaledLongitude(srcPts[srcOff], 
θ_bound);     // θ = Δλ⋅n
-            final double φ    = srcPts[srcOff + 1];                         // 
Sign may be reversed
+            final double θ    = srcPts[srcOff  ];       // θ = Δλ⋅n
+            final double φ    = srcPts[srcOff+1];       // Sign may be reversed
             final double absφ = abs(φ);
             final double sinθ = sin(θ);
             final double cosθ = cos(θ);
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
new file mode 100644
index 0000000000..98da9b1472
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
@@ -0,0 +1,347 @@
+/*
+ * 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.referencing.operation.projection;
+
+import java.util.Optional;
+import java.io.Serializable;
+import org.opengis.geometry.Envelope;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform2D;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.AbstractMathTransform2D;
+import org.apache.sis.referencing.operation.transform.ContextualParameters;
+import org.apache.sis.referencing.operation.transform.DomainDefinition;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.internal.util.DoubleDouble;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.measure.Longitude;
+
+
+/**
+ * If the scaled longitude θ=n⋅λ is outside the [−n⋅π … n⋅π] range, maybe 
shifts θ to that range.
+ * This transform intentionally does <strong>not</strong> force θ to be inside 
that range in all cases.
+ * We avoid explicit wraparounds as much as possible (as opposed to implicit 
wraparounds performed by
+ * trigonometric functions) because they tend to introduce discontinuities. We 
perform wraparounds only
+ * when necessary for the problem of area crossing the anti-meridian (±180°).
+ *
+ * <div class="note"><b>Example:</b>
+ * a CRS for Alaska may have the central meridian at λ₀=−154° of longitude. If 
the point to project is
+ * at λ=177° of longitude, calculations will be performed with Δλ=331° while 
the correct value that we
+ * need to use is Δλ=−29°.</div>
+ *
+ * In order to avoid wraparound operations as much as possible, we test only 
the bound where anti-meridian
+ * problem may happen; no wraparound will be applied for the opposite bound. 
Furthermore we add or subtract
+ * 360° only once. Even if the point did many turns around the Earth, the 360° 
shift will still be applied
+ * at most once. The desire to apply the minimal amount of shifts is the 
reason why we do not use
+ * {@link Math#IEEEremainder(double, double)}.
+ *
+ * <h2>When to use</h2>
+ * Many map projections implicitly wraparound longitude values through the use 
of trigonometric functions
+ * ({@code sin(λ)}, {@code cos(λ)}, <i>etc</i>). For those map projections, 
the wraparound is unconditional
+ * and this {@code LongitudeWraparound} class is not needed. This class is 
used only when the wraparound is
+ * not implicitly done and the central meridian is not zero. The latter 
condition is because subtraction of
+ * central meridian may cause longitude values to go outside the −180° … +180° 
range.
+ *
+ * <p>This transform is hidden in WKT (it does not appear as a 
concatenation).</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ *
+ * @see org.apache.sis.referencing.operation.transform.WraparoundTransform
+ * @see <a href="https://issues.apache.org/jira/browse/SIS-486";>SIS-486</a>
+ *
+ * @since 1.3
+ * @module
+ */
+final class LongitudeWraparound extends AbstractMathTransform2D implements 
Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -4658152274068444690L;
+
+    /**
+     * The actual map projection to execute after the longitude wraparound.
+     */
+    final NormalizedProjection projection;
+
+    /**
+     * A bound of the [−n⋅π … n⋅π] range which, if exceeded, should cause 
wraparound.
+     * Some (not all) θ = n⋅λ values need to be shifted inside that range 
before to
+     * use them in trigonometric functions.
+     *
+     * <p>The sign is significant. A negative value means that the wraparound 
is applied
+     * only on longitudes less than −180°. A positive value means that the 
wraparound is
+     * applied only on longitudes greater than +180°.</p>
+     */
+    final double bound;
+
+    /**
+     * Whether the bound is on the side of negative longitudes. This is {@code 
bound < 0}.
+     * This field is trivial in {@code LongitudeWraparound} case, but more 
important in
+     * {@link Inverse} case because {@code rotation - bound} may cancel to 
zero.
+     */
+    final boolean negative;
+
+    /**
+     * The inverse of this operation.
+     *
+     * @see #inverse()
+     */
+    private final Inverse inverse;
+
+    /**
+     * Creates a new transform for wrapping around the longitude values before 
a map projection.
+     *
+     * @param  projection  the actual map projection to execute after the 
longitude wraparound.
+     * @param  bound       one bound of the [−n⋅π … n⋅π] range, on the side 
where wraparound needs to be applied.
+     * @param  rotation    longitude rotation applied by the normalization 
matrix after conversion to projection domain.
+     */
+    LongitudeWraparound(final NormalizedProjection projection, final double 
bound, final double rotation) {
+        this.projection = projection;
+        this.bound = bound;
+        negative = bound < 0;
+        inverse = new Inverse(this, rotation - bound);
+    }
+
+    /**
+     * Returns a bound of the [−n⋅π … n⋅π] domain where <var>n</var> is a map 
projection dependent factor.
+     * The factor is inferred from the {@link NormalizedProjection#context}.
+     *
+     * @param  normalize  the normalization matrix of the projection for which 
to get a bound.
+     * @param  negative   {@code true} for the −180° bound, or {@code false} 
for the +180° bound.
+     * @return a bound of the [−n⋅π … n⋅π] range.
+     */
+    static double boundOfScaledLongitude(final MatrixSIS normalize, final 
boolean negative) {
+        final DoubleDouble bound = 
DoubleDouble.castOrCopy(normalize.getNumber(0, 0));
+        bound.multiply(negative ? Longitude.MIN_VALUE : Longitude.MAX_VALUE);
+        return bound.doubleValue();
+    }
+
+    /**
+     * Returns the ranges of coordinate values which can be used as inputs.
+     * The {@link #projection} domain is used verbatim, without wraparound 
adjustment.
+     *
+     * @param  criteria  controls the definition of transform domain.
+     * @return estimation of a domain where this transform is considered 
numerically applicable.
+     */
+    @Override
+    public Optional<Envelope> getDomain(DomainDefinition criteria) throws 
TransformException {
+        return projection.getDomain(criteria);
+    }
+
+    /**
+     * Returns the parameter descriptors for this math transform, or {@code 
null} if unknown.
+     * Delegates to {@link #projection} since this {@code LongitudeWraparound} 
is hidden.
+     * This is used by WKT formatting.
+     */
+    @Override
+    public ParameterDescriptorGroup getParameterDescriptors() {
+        return projection.getParameterDescriptors();
+    }
+
+    /**
+     * Returns the parameter values for this math transform, or {@code null} 
if unknown.
+     * Delegates to {@link #projection} since this {@code LongitudeWraparound} 
is hidden.
+     * This is used by WKT formatting.
+     */
+    @Override
+    public ParameterValueGroup getParameterValues() {
+        return projection.getParameterValues();
+    }
+
+    /**
+     * Returns the parameters for a sequence of <cite>normalize</cite> → 
{@code this} → <cite>denormalize</cite>.
+     * Delegates to {@link #projection} since this {@code LongitudeWraparound} 
is hidden.
+     * This is used by WKT formatting.
+     */
+    @Override
+    protected ContextualParameters getContextualParameters() {
+        return projection.getContextualParameters();
+    }
+
+    /**
+     * Transforms a single coordinate tuple in an array, and optionally 
computes the transform derivative.
+     * The wraparound is applied, if needed, on the longitude value before to 
delegate to {@link #projection}.
+     */
+    @Override
+    public Matrix transform(final double[] srcPts, final int srcOff,
+                            final double[] dstPts, final int dstOff, boolean 
derivate) throws TransformException
+    {
+        final double λ = srcPts[srcOff];
+        if (negative ? λ < bound : λ > bound) {
+            dstPts[dstOff+1] = srcPts[srcOff+1];        // Must be first.
+            dstPts[dstOff  ] = λ - 2*bound;
+            return projection.transform(dstPts, dstOff, dstPts, dstOff, 
derivate);
+        } else {
+            return projection.transform(srcPts, srcOff, dstPts, dstOff, 
derivate);
+        }
+    }
+
+    /**
+     * Transforms a list of coordinate tuples. This method is provided for 
efficiently transforming many points.
+     * Wraparound is applied on all longitude values before to delegate to 
{@link #projection}.
+     */
+    @Override
+    public void transform(final double[] srcPts, final int srcOff,
+                          final double[] dstPts, final int dstOff, final int 
numPts) throws TransformException
+    {
+        if (srcPts != dstPts || srcOff != dstOff) {
+            System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 
DIMENSION);
+        }
+        final double period = 2*bound;
+        final int stop = dstOff + numPts * DIMENSION;
+        for (int i=dstOff; i<stop; i+=DIMENSION) {
+            final double λ = dstPts[i];
+            if (negative ? λ < bound : λ > bound) {
+                dstPts[i] = λ - period;
+            }
+        }
+        projection.transform(dstPts, dstOff, dstPts, dstOff, numPts);
+    }
+
+    /**
+     * Returns the inverse transform of this object.
+     */
+    @Override
+    public MathTransform2D inverse() throws NoninvertibleTransformException {
+        return inverse;
+    }
+
+    /**
+     * Longitude wraparound applied on reverse projection.
+     * This is a copy of {@code NormalizedProjection.Inverse} with longitude 
wraparound added after conversion.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.3
+     * @since   1.3
+     * @module
+     */
+    private static final class Inverse extends AbstractMathTransform2D.Inverse 
implements Serializable {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = -543869926271003589L;
+
+        /**
+         * The projection to reverse, which is the enclosing transform.
+         */
+        private final LongitudeWraparound forward;
+
+        /**
+         * {@link LongitudeWraparound#bound} with opposite sign and translated 
by the longitude rotation.
+         * This is the bound that the reverse projection needs to use before 
longitude rotation.
+         * This bound <strong>shall not</strong> be used for period 
computation.
+         */
+        private final double bound;
+
+        /**
+         * Creates a reverse projection for the given forward projection.
+         */
+        Inverse(final LongitudeWraparound forward, final double bound) {
+            this.forward = forward;
+            this.bound = bound;
+        }
+
+        /**
+         * Returns the inverse of this math transform, which is the forward 
projection.
+         */
+        @Override
+        public MathTransform2D inverse() {
+            return forward;
+        }
+
+        /**
+         * Reverse projects the specified {@code srcPts} and stores the result 
in {@code dstPts}.
+         * If the derivative has been requested, then this method will 
delegate the derivative
+         * calculation to the enclosing class and inverts the resulting matrix.
+         */
+        @Override
+        public Matrix transform(final double[] srcPts, final int srcOff,
+                                      double[] dstPts,       int dstOff,
+                                final boolean derivate) throws 
TransformException
+        {
+            if (derivate && dstPts == null) {
+                dstPts = new double[DIMENSION];
+                dstOff = 0;
+            }
+            forward.projection.inverseTransform(srcPts, srcOff, dstPts, 
dstOff);
+            final Matrix d = derivate ? 
Matrices.inverse(forward.transform(dstPts, dstOff, null, 0, true)) : null;
+            final double λ = dstPts[dstOff];
+            if (forward.negative ? λ > bound : λ < bound) {          // 
Interpretation of `negative` is inversed.
+                dstPts[dstOff] = λ + 2*forward.bound;
+            }
+            return d;
+        }
+
+        /**
+         * Inverse transforms an arbitrary amount of coordinates. This method 
optimizes the
+         * case where conversions can be applied by a loop with indices in 
increasing order.
+         */
+        @Override
+        public void transform(final double[] srcPts, int srcOff,
+                              final double[] dstPts, int dstOff, int numPts) 
throws TransformException
+        {
+            if (srcPts == dstPts && srcOff < dstOff) {
+                super.transform(srcPts, srcOff, dstPts, dstOff, numPts);
+            } else {
+                final double period = 2 * forward.bound;
+                while (--numPts >= 0) {
+                    forward.projection.inverseTransform(srcPts, srcOff, 
dstPts, dstOff);
+                    final double λ = dstPts[dstOff];
+                    if (forward.negative ? λ > bound : λ < bound) {     // 
Interpretation of `negative` is inversed.
+                        dstPts[dstOff] = λ + period;
+                    }
+                    srcOff += DIMENSION;
+                    dstOff += DIMENSION;
+                }
+            }
+        }
+    }
+
+    /*
+     * We do not implement `tryConcatenate` yet because the result of invoking 
`projection.tryConcatenate(…)`
+     * is either null or a linear transform. In the latter case, the linear 
transform can not be wrapped by
+     * this `longitudeWraparound` class. Even if we could, it would block 
concatenation with surrounding
+     * affine transforms. We have no easy solution for now.
+     */
+
+    /**
+     * Computes a hash value for this transform.
+     */
+    @Override
+    protected int computeHashCode() {
+        return projection.hashCode() + Double.hashCode(bound);
+    }
+
+    /**
+     * Compares the specified object with this math transform for equality.
+     */
+    @Override
+    public boolean equals(final Object object, final ComparisonMode mode) {
+        if (object instanceof LongitudeWraparound) {
+            final LongitudeWraparound that = (LongitudeWraparound) object;
+            return Numerics.epsilonEqual(bound, that.bound, mode)
+                      && projection.equals(that.projection, mode);
+        }
+        return false;
+    }
+}
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
index 1059bd8a6a..d6be730fbb 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -195,12 +195,11 @@ public class Mercator extends ConformalProjection {
         final Variant variant = variant(method, Variant.values(), 
Variant.TWO_PARALLELS);
         final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new 
EnumMap<>(ParameterRole.class);
         /*
-         * "Longitude of origin" is a parameter of all Mercator projections, 
but is intentionally omitted from
-         * this map because it will be handled in a special way by the 
Mercator constructor. The "scale factor"
-         * is not formally a "Mercator 2SP" argument, but we accept it anyway 
for all Mercator projections
-         * since it may be used in some Well Known Text (WKT).
+         * The "scale factor" is not formally a "Mercator 2SP" argument, but 
we accept it anyway
+         * for all Mercator projections because it may be used in some Well 
Known Text (WKT).
          */
-        roles.put(ParameterRole.SCALE_FACTOR, Mercator1SP.SCALE_FACTOR);
+        roles.put(ParameterRole.SCALE_FACTOR,     Mercator1SP.SCALE_FACTOR);
+        roles.put(ParameterRole.CENTRAL_MERIDIAN, 
Mercator1SP.LONGITUDE_OF_ORIGIN);
         switch (variant) {
             case REGIONAL: {
                 roles.put(ParameterRole.FALSE_EASTING,  
RegionalMercator.EASTING_AT_FALSE_ORIGIN);
@@ -270,31 +269,10 @@ public class Mercator extends ConformalProjection {
          * if they really want, since we sometime see such CRS definitions.
          */
         final double φ1 = 
toRadians(initializer.getAndStore(Mercator2SP.STANDARD_PARALLEL));
-        final DoubleDouble k0 = new DoubleDouble(initializer.scaleAtφ(sin(φ1), 
cos(φ1)));
-        final DoubleDouble ku = DoubleDouble.createAndGuessError(PI/0.5);
-        ku.multiply(k0);
-        /*
-         * Note about `ku`: in principle we should convert longitude values to 
radians, then apply the same
-         * scale factor k₀ to both longitude and latitude values. However in 
this implementation instead of
-         * converting the [−180 … 180]° range to the [−π … π] range, we rather 
convert to a [−½ … ½] range.
-         * This is because longitude conversions in Mercator projection do not 
use trigonometric functions,
-         * so we do not get the implicit wraparound performed by trigonometric 
functions such as sin(λ−λ₀).
-         * We will use `Math.rint(…)` instead.
-         */
+        final Number k0 = new DoubleDouble(initializer.scaleAtφ(sin(φ1), 
cos(φ1)));
         final MatrixSIS normalize   = 
context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
         final MatrixSIS denormalize = 
context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
-        final DoubleDouble forUnity = 
DoubleDouble.createAndGuessError(1.0/360);
-        normalize.setNumber(0, 0, forUnity);           // Overwrite the 
"degrees to radians" coefficient.
-        if (λ0 != 0) {
-            /*
-             * Use double-double arithmetic here for consistency with the work 
done in the normalization matrix.
-             * The intent is to have exact value at `double` precision when 
computing Matrix.invert(). Note that
-             * there is no such goal for other parameters computed from sine 
or consine functions.
-             */
-            forUnity.multiplyGuessError(-λ0);
-            normalize.setNumber(0, 2, forUnity);
-        }
-        denormalize.convertBefore(0, ku, null);
+        denormalize.convertBefore(0, k0, null);
         denormalize.convertBefore(1, k0, null);
         if (φ0 != 0) {
             denormalize.convertBefore(1, null, new DoubleDouble(-log(expΨ(φ0, 
eccentricity * sin(φ0)))));
@@ -391,7 +369,7 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
             }
             kernel = new Spherical(this);
         }
-        return context.completeTransform(factory, kernel);
+        return kernel.completeWithWraparound(factory);
     }
 
     /**
@@ -399,12 +377,17 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
      * the limit is arbitrarily set to 84° of latitude North and South. This 
is consistent with the
      * "World Mercator" domain of validity defined by EPSG:3395, which is 80°S 
to 84°N.
      *
+     * <p>The range of longitude values is set to an arbitrary range larger 
than −180° … +180°,
+     * because the Mercator projection is mathematically capable to handle 
coordinates beyond that range
+     * even if those coordinates have no real world meaning. This expansion 
can facilitate the projection
+     * of envelopes, geometries or rasters.</p>
+     *
      * @since 1.3
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        final double limit = (variant == Variant.MILLER) ? -PI/2 : -PI/2 * 
(84d/90);
-        return Optional.of(new Envelope2D(null, -0.5, limit, 1, -2*limit));
+        final double limit = (variant == Variant.MILLER) ? -PI/2 : 
-POLAR_AREA_LIMIT;
+        return Optional.of(new Envelope2D(null, -LARGE_LONGITUDE_LIMIT, limit, 
2*LARGE_LONGITUDE_LIMIT, -2*limit));
     }
 
     /**
@@ -421,7 +404,6 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                             final double[] dstPts, final int dstOff,
                             final boolean derivate) throws ProjectionException
     {
-        final double x    = srcPts[srcOff  ];
         final double φ    = srcPts[srcOff+1];
         final double sinφ = sin(φ);
         if (dstPts != null) {
@@ -447,7 +429,7 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                     y = NaN;
                 }
             }
-            dstPts[dstOff  ] = x - rint(x);     // Scale will be applied by 
the denormalization matrix.
+            dstPts[dstOff  ] = srcPts[srcOff];   // Scale will be applied by 
the denormalization matrix.
             dstPts[dstOff+1] = y;
         }
         /*
@@ -467,21 +449,24 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                           final double[] dstPts, int dstOff, int numPts)
             throws TransformException
     {
-        if ((srcPts == dstPts && srcOff < dstOff) || getClass() != 
Mercator.class) {
+        if (srcPts != dstPts || srcOff != dstOff || getClass() != 
Mercator.class) {
             super.transform(srcPts, srcOff, dstPts, dstOff, numPts);
         } else {
+            /*
+             * Override the super-class method only as an optimization in the 
special case where the target coordinates
+             * are written at the same locations than the source coordinates. 
In such case, we can take advantage of
+             * the fact that the λ values are not modified by the normalized 
Mercator projection.
+             */
+            dstOff--;
             while (--numPts >= 0) {
-                final double x = srcPts[srcOff++];
-                final double φ = srcPts[srcOff++];
-                final double y;
-                if (φ == 0) {
-                    y = φ;
-                } else {
+                final double φ = dstPts[dstOff += DIMENSION];                  
 // Same as srcPts[srcOff + 1].
+                if (φ != 0) {
                     /*
                      * See the javadoc of the Spherical inner class for a note
                      * about why we perform explicit checks for the pole cases.
                      */
                     final double a = abs(φ);
+                    final double y;
                     if (a < PI/2) {
                         y = log(expΨ(φ, eccentricity * sin(φ)));
                     } else if (a <= (PI/2 + ANGULAR_TOLERANCE)) {
@@ -489,9 +474,8 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                     } else {
                         y = NaN;
                     }
+                    dstPts[dstOff] = y;
                 }
-                dstPts[dstOff++] = x - rint(x);
-                dstPts[dstOff++] = y;
             }
         }
     }
@@ -561,7 +545,6 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                                 final double[] dstPts, final int dstOff,
                                 final boolean derivate)
         {
-            final double x = srcPts[srcOff  ];
             final double φ = srcPts[srcOff+1];
             if (dstPts != null) {
                 /*
@@ -583,7 +566,7 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                         y = NaN;
                     }
                 }
-                dstPts[dstOff  ] = x - rint(x);
+                dstPts[dstOff  ] = srcPts[srcOff];
                 dstPts[dstOff+1] = y;
             }
             return derivate ? new Matrix2(1, 0, 0, 1/cos(φ)) : null;
@@ -601,18 +584,16 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                               final double[] dstPts, int dstOff, int numPts)
                 throws TransformException
         {
-            if (srcPts == dstPts && srcOff < dstOff) {
+            if (srcPts != dstPts || srcOff != dstOff) {
                 super.transform(srcPts, srcOff, dstPts, dstOff, numPts);
             } else {
+                dstOff--;
                 while (--numPts >= 0) {
-                    final double x = srcPts[srcOff++];
-                    final double φ = srcPts[srcOff++];
-                    final double y;
-                    if (φ == 0) {
-                        y = φ;
-                    } else {
+                    final double φ = dstPts[dstOff += DIMENSION];              
 // Same as srcPts[srcOff + 1].
+                    if (φ != 0) {
                         // See class javadoc for a note about explicit check 
for poles.
                         final double a = abs(φ);
+                        final double y;
                         if (a < PI/2) {
                             y = log(tan(PI/4 + 0.5*φ));                        
 // Part of Snyder (7-2)
                         } else if (a <= (PI/2 + ANGULAR_TOLERANCE)) {
@@ -620,9 +601,8 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
                         } else {
                             y = NaN;
                         }
+                        dstPts[dstOff] = y;
                     }
-                    dstPts[dstOff++] = x - rint(x);
-                    dstPts[dstOff++] = y;
                 }
             }
         }
@@ -644,7 +624,7 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
      * Invoked when {@link #tryConcatenate(boolean, MathTransform, 
MathTransformFactory)} detected a
      * (inverse) → (affine) → (this) transforms sequence. If the affine 
transform in the middle does
      * not change the latitude value, then we can take advantage of the fact 
that longitude conversion
-     * is linear (ignoring wraparound).
+     * is linear.
      */
     @Override
     final MathTransform tryConcatenate(boolean projectedSpace, Matrix affine, 
MathTransformFactory factory)
@@ -653,7 +633,7 @@ subst:  if (variant.spherical || (eccentricity == 0 && 
getClass() == Mercator.cl
         /*
          * Verify that the latitude row is an identity conversion except for 
the sign which is allowed to change
          * (but no scale and no translation are allowed).  Ignore the 
longitude row because it just pass through
-         * this Mercator projection with no impact on any calculation (if we 
ignore the wraparound).
+         * this Mercator projection with no impact on any calculation.
          */
         if (affine.getElement(1,0) == 0 && affine.getElement(1, DIMENSION) == 
0 && Math.abs(affine.getElement(1,1)) == 1) {
             if (factory != null) {
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
index aeb3fa02e5..17a14be96a 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
@@ -132,10 +132,10 @@ public class Mollweide extends NormalizedProjection {
                             final double[] dstPts, final int dstOff,
                             final boolean derivate) throws ProjectionException
     {
-        final double λ = srcPts[srcOff];
+        final double λ = srcPts[srcOff];            // Scaled by 2√2.
         final double φ = srcPts[srcOff + 1];
         final double sinφ = sin(φ);
-        double θp = 2 * asin(φ / (PI/2));           // θ′ in Snyder formulas.
+        double θp = 2 * asin(φ * (2/PI));           // θ′ in Snyder formulas.
         /*
          * If sin(φ) is 1 or -1 we are on a pole.
          * Iteration would produce NaN values.
@@ -182,7 +182,7 @@ public class Mollweide extends NormalizedProjection {
         final double y  = srcPts[srcOff + 1];
         final double θ  = asin(y);
         final double θp = 2 * θ;
-        final double φ  = asin((θp + sin(θp)) / PI);
+        final double φ  = asin((θp + sin(θp)) * (1/PI));
         double λ = x / cos(θ);
         if (abs(λ) > 2*SQRT_2*PI) {
             λ = Double.NaN;
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
index 6717376c01..39121747fd 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
@@ -51,6 +51,7 @@ import 
org.apache.sis.referencing.operation.transform.AbstractMathTransform2D;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
 import org.apache.sis.referencing.operation.transform.MathTransformProvider;
+import org.apache.sis.referencing.operation.transform.DomainDefinition;
 import org.apache.sis.internal.referencing.provider.MapProjection;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.Formulas;
@@ -177,6 +178,22 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
      */
     static final int MAXIMUM_ITERATIONS = Formulas.MAXIMUM_ITERATIONS;
 
+    /**
+     * Arbitrary latitude threshold (in radians) for considering that a point 
is in the polar area.
+     * This is used for implementations of the {@link 
#getDomain(DomainDefinition)} method.
+     */
+    static final double POLAR_AREA_LIMIT = PI/2 * (84d/90);
+
+    /**
+     * An arbitrarily large longitude value (in radians) for map projections 
capable to have infinite
+     * extent in the east-west direction. This is the case of {@link Mercator} 
projection for example.
+     * Longitudes have no real world meaning outside −180° … +180° range 
(unless wraparound is applied),
+     * but we nevertheless accept large values without wraparound because they 
make envelope projection easier.
+     * This is used for implementations of the {@link 
#getDomain(DomainDefinition)} method,
+     * which use arbitrary limits anyway.
+     */
+    static final double LARGE_LONGITUDE_LIMIT = 100*PI;
+
     /**
      * The internal parameter descriptors. Keys are implementation classes.  
Values are parameter descriptor groups
      * containing at least a parameter for the {@link #eccentricity} value, 
and optionally other internal parameter
@@ -511,6 +528,48 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
         return context.completeTransform(factory, this);
     }
 
+    /**
+     * Returns a transform which may shift scaled longitude θ=n⋅λ inside the 
[−n⋅π … n⋅π] range.
+     * The transform intentionally does <strong>not</strong> force θ to be 
inside that range in all cases.
+     * We avoid explicit wraparounds as much as possible (as opposed to 
implicit wraparounds performed by
+     * trigonometric functions) because they tend to introduce 
discontinuities. We perform wraparounds only
+     * when necessary for the problem of area crossing the anti-meridian 
(±180°).
+     *
+     * <div class="note"><b>Example:</b>
+     * a CRS for Alaska may have the central meridian at λ₀=−154° of 
longitude. If the point to project is
+     * at λ=177° of longitude, calculations will be performed with Δλ=331° 
while the correct value that we
+     * need to use is Δλ=−29°.</div>
+     *
+     * In order to avoid wraparound operations as much as possible, we test 
only the bound where anti-meridian
+     * problem may happen; no wraparound will be applied for the opposite 
bound. Furthermore we add or subtract
+     * 360° only once. Even if the point did many turns around the Earth, the 
360° shift will still be applied
+     * at most once. The desire to apply the minimal amount of shifts is the 
reason why we do not use
+     * {@link Math#IEEEremainder(double, double)}.
+     *
+     * <h4>When to use</h4>
+     * This method is invoked by map projections that multiply the longitude 
values by some scale factor before
+     * to use them in trigonometric functions. Usually we do not explicitly 
wraparound the longitude values,
+     * because trigonometric functions do that automatically for us. However 
if the longitude is multiplied
+     * by some factor before to be used in trigonometric functions, then that 
implicit wraparound is not the
+     * one we expect. The map projection code needs to perform explicit 
wraparound in such cases.
+     *
+     * @param  factory  the factory to use for completing the transform with 
normalization/denormalization steps.
+     * @return the map projection from (λ,φ) to (<var>x</var>,<var>y</var>) 
coordinates with wraparound if needed.
+     * @throws FactoryException if an error occurred while creating a 
transform.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-486";>SIS-486</a>
+     */
+    final MathTransform completeWithWraparound(final MathTransformFactory 
factory) throws FactoryException {
+        MathTransform kernel = this;
+        final MatrixSIS normalize = 
context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
+        final double rotation = normalize.getElement(0, DIMENSION);
+        if (rotation != 0 && Double.isFinite(rotation)) {                 // 
Finite check is paranoiac (shall always be true).
+            kernel = new LongitudeWraparound(this,
+                    LongitudeWraparound.boundOfScaledLongitude(normalize, 
rotation < 0), rotation);
+        }
+        return context.completeTransform(factory, kernel);
+    }
+
     /**
      * If this map projection can not handle the parameters given by the user 
but an other projection could, delegates
      * to the other projection. This method can be invoked by some {@link 
#createMapProjection(MathTransformFactory)}
@@ -649,37 +708,26 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
     }
 
     /**
-     * If the given scaled longitude θ=n⋅λ is outside the [−n⋅π … n⋅π] range, 
maybe shifts θ to that range.
-     * This method intentionally does <strong>not</strong> force θ to be 
inside that range in all cases.
-     * We avoid explicit wraparounds as much as possible (as opposed to 
implicit wraparounds performed by
-     * trigonometric functions) because they tend to introduce 
discontinuities. We perform wraparounds only
-     * when necessary for the problem of area crossing the anti-meridian 
(±180°).
-     *
-     * <div class="note"><b>Example:</b>
-     * a CRS for Alaska may have the central meridian at λ₀=−154° of 
longitude. If the point to project is
-     * at λ=177° of longitude, calculations will be performed with Δλ=331° 
while the correct value that we
-     * need to use is Δλ=−29°.</div>
+     * The longitude value where wraparound is, or would be, applied by this 
map projection.
+     * This is typically {@link Math#PI} (180° converted to radians) but not 
necessarily,
+     * because implementations are free to scale the longitude values by an 
arbitrary factor.
      *
-     * In order to avoid wraparound operations as much as possible, we test 
only the bound where anti-meridian
-     * problem may happen; no wraparound will be applied for the opposite 
bound. Furthermore we add or subtract
-     * 360° only once. Even if the point did many turns around the Earth, the 
360° shift will still be applied
-     * at most once. The desire to apply the minimal amount of shifts is the 
reason why we do not use
-     * {@link Math#IEEEremainder(double, double)}.
+     * <p>The wraparound may not be really applied by the {@code transform(…)} 
methods.
+     * Many map projections implicitly wraparound longitude values through the 
use of trigonometric functions
+     * ({@code sin(λ)}, {@code cos(λ)}, <i>etc</i>). For those map 
projections, the wraparound is unconditional.
+     * But some other map projections are capable to handle longitude values 
beyond the [−180° … +180°] range
+     * as if the world was expanding toward infinity in east and west 
directions.
+     * The most common example is the {@linkplain Mercator} projection.
+     * In those latter cases, wraparounds are avoided as much as possible in 
order to facilitate the projection
+     * of envelopes, geometries or rasters, where discontinuities (sudden 
jumps of 360°) cause artifacts.</p>
      *
-     * @param  θ        the scaled longitude value θ=n⋅λ where <var>n</var> is 
a projection-dependent factor.
-     * @param  θ_bound  minimal (if negative) or maximal (if positive) value 
of θ before to apply the shift.
-     *                  This is computed by <code>{@linkplain 
Initializer#boundOfScaledLongitude(double)
-     *                  Initializer.boundOfScaledLongitude}(n)</code>
-     * @return θ or shifted θ.
+     * @return the longitude value where wraparound is or would be applied.
      *
-     * @see Initializer#boundOfScaledLongitude(double)
-     * @see <a href="https://issues.apache.org/jira/browse/SIS-486";>SIS-486</a>
+     * @see #getDomain(DomainDefinition)
      */
-    static double wraparoundScaledLongitude(double θ, final double θ_bound) {
-        if (θ_bound < 0 ? θ < θ_bound : θ > θ_bound) {
-            θ -= 2*θ_bound;
-        }
-        return θ;
+    final double getWraparoundLongitude() {
+        return LongitudeWraparound.boundOfScaledLongitude(
+                
context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION), false);
     }
 
     /*
@@ -775,7 +823,9 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
     }
 
     /**
-     * Inverse of a normalized map projection.
+     * Reverse of a normalized map projection.
+     * Note that a slightly modified copy of this class is in {@code 
LongitudeWraparound.Inverse}.
+     * If this is class is modified, consider updating {@code 
LongitudeWraparound.Inverse} accordingly.
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.0
@@ -789,19 +839,19 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
         private static final long serialVersionUID = 6014176098150309651L;
 
         /**
-         * The enclosing transform.
+         * The projection to reverse, which is the enclosing transform.
          */
         private final NormalizedProjection forward;
 
         /**
-         * Default constructor.
+         * Creates a reverse projection for the given forward projection.
          */
         Inverse(final NormalizedProjection forward) {
             this.forward = forward;
         }
 
         /**
-         * Returns the inverse of this math transform.
+         * Returns the inverse of this math transform, which is the forward 
projection.
          */
         @Override
         public MathTransform2D inverse() {
@@ -809,7 +859,7 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
         }
 
         /**
-         * Inverse transforms the specified {@code srcPts} and stores the 
result in {@code dstPts}.
+         * Reverse projects the specified {@code srcPts} and stores the result 
in {@code dstPts}.
          * If the derivative has been requested, then this method will 
delegate the derivative
          * calculation to the enclosing class and inverts the resulting matrix.
          */
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
index 7e71293a4d..9d2f3eb0b2 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
@@ -63,7 +63,7 @@ import static 
org.apache.sis.internal.referencing.provider.ObliqueStereographic.
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Emmanuel Giasson (Thales)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -71,7 +71,7 @@ public class ObliqueStereographic extends 
NormalizedProjection {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -1725537881127730658L;
+    private static final long serialVersionUID = -1454098847621943639L;
 
     /**
      * Conformal latitude of origin (χ₀), together with its sine and cosine.
@@ -94,15 +94,6 @@ public class ObliqueStereographic extends 
NormalizedProjection {
      */
     private final double g, h;
 
-    /**
-     * A bound of the [−n⋅π … n⋅π] range, which is the valid range of  θ = n⋅λ 
 values.
-     * Some (not all) θ values need to be shifted inside that range before to 
use them
-     * in trigonometric functions.
-     *
-     * @see Initializer#boundOfScaledLongitude(DoubleDouble)
-     */
-    private final double θ_bound;
-
     /**
      * Creates an Oblique Stereographic projection from the given parameters.
      * The {@code method} argument can be the description of one of the 
following:
@@ -184,7 +175,6 @@ public class ObliqueStereographic extends 
NormalizedProjection {
         final double R2 = 2 * initializer.radiusOfConformalSphere(sinφ0);
         denormalize.convertBefore(0, R2, null);
         denormalize.convertBefore(1, R2, null);
-        θ_bound = initializer.boundOfScaledLongitude(n);
     }
 
     /**
@@ -192,14 +182,13 @@ public class ObliqueStereographic extends 
NormalizedProjection {
      */
     ObliqueStereographic(final ObliqueStereographic other) {
         super(null, other);
-        χ0      = other.χ0;
-        sinχ0   = other.sinχ0;
-        cosχ0   = other.cosχ0;
-        c       = other.c;
-        n       = other.n;
-        g       = other.g;
-        h       = other.h;
-        θ_bound = other.θ_bound;
+        χ0    = other.χ0;
+        sinχ0 = other.sinχ0;
+        cosχ0 = other.cosχ0;
+        c     = other.c;
+        n     = other.n;
+        g     = other.g;
+        h     = other.h;
     }
 
     /**
@@ -249,11 +238,10 @@ public class ObliqueStereographic extends 
NormalizedProjection {
                 return delegate(factory, PolarStereographicA.NAME);
             }
         }
-        ObliqueStereographic kernel = this;
         if (eccentricity == 0 && getClass() == ObliqueStereographic.class) {
-            kernel = new Spherical(this);
+            return context.completeTransform(factory, new Spherical(this));
         }
-        return context.completeTransform(factory, kernel);
+        return completeWithWraparound(factory);
     }
 
     /**
@@ -271,9 +259,8 @@ public class ObliqueStereographic extends 
NormalizedProjection {
                             final double[] dstPts, final int dstOff,
                             final boolean derivate) throws ProjectionException
     {
-        // Λ = λ⋅n  (see below), ignoring longitude of origin.
-        final double Λ     = wraparoundScaledLongitude(srcPts[srcOff], 
θ_bound);
-        final double φ     = srcPts[srcOff + 1];
+        final double Λ     = srcPts[srcOff  ];      // Λ = λ⋅n  (see below), 
ignoring longitude of origin.
+        final double φ     = srcPts[srcOff+1];
         final double sinφ  = sin(φ);
         final double ℯsinφ = eccentricity * sinφ;
         final double Sa    = (1 +  sinφ) / (1 -  sinφ);
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
index 454920c019..039f1ac336 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
@@ -17,10 +17,13 @@
 package org.apache.sis.referencing.operation.projection;
 
 import java.util.EnumMap;
+import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.InvalidParameterValueException;
 import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.OperationMethod;
-import org.opengis.parameter.InvalidParameterValueException;
 import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
@@ -61,7 +64,7 @@ import static 
org.apache.sis.internal.referencing.provider.SatelliteTracking.*;
  *
  * @author  Matthieu Bastianelli (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -69,7 +72,7 @@ public class SatelliteTracking extends NormalizedProjection {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -209787336760184649L;
+    private static final long serialVersionUID = 859940667477896653L;
 
     /**
      * Sines and cosines of inclination between the plane of the Earth's 
Equator and the plane
@@ -98,15 +101,6 @@ public class SatelliteTracking extends NormalizedProjection 
{
      */
     private final boolean isConic;
 
-    /**
-     * A bound of the [−n⋅π … n⋅π] range, which is the valid range of  θ = n⋅λ 
 values.
-     * Some (not all) θ values need to be shifted inside that range before to 
use them
-     * in trigonometric functions.
-     *
-     * @see Initializer#boundOfScaledLongitude(DoubleDouble)
-     */
-    private final double θ_bound;
-
     /**
      * Work around for RFE #4093999 in Sun's bug database ("Relax constraint on
      * placement of this()/super() call in constructors").
@@ -203,7 +197,6 @@ public class SatelliteTracking extends NormalizedProjection 
{
             normalize  .convertAfter (0,  n,  null);
             denormalize.convertBefore(0, +ρf, null);
             denormalize.convertBefore(1, -ρf, ρ0);
-            θ_bound = initializer.boundOfScaledLongitude(n);
         } else {
             /*
              * Cylindrical projection case. The equations are (ignoring R and 
λ₀):
@@ -215,7 +208,7 @@ public class SatelliteTracking extends NormalizedProjection 
{
              * The cosφ₁ (for x at dimension 0) and cosφ₁/F₁′ (for y at 
dimension 1) factors are computed
              * in advance and stored below. The remaining factor to compute in 
transform(…) method is L.
              */
-            n = s0 = θ_bound = Double.NaN;
+            n = s0 = Double.NaN;
             final double cotF = sqrt(cos2_φ1 - cos2_i) / (p2_on_p1*cos2_φ1 - 
cos_i);    // Cotangente of F₁.
             denormalize.convertBefore(0, cosφ1,      null);
             denormalize.convertBefore(1, cosφ1*cotF, null);
@@ -225,6 +218,24 @@ public class SatelliteTracking extends 
NormalizedProjection {
         }
     }
 
+    /**
+     * Returns the sequence of <cite>normalization</cite> → {@code this} → 
<cite>denormalization</cite> transforms
+     * as a whole. The transform returned by this method expects 
(<var>longitude</var>, <var>latitude</var>)
+     * coordinates in <em>degrees</em> and returns (<var>x</var>,<var>y</var>) 
coordinates in <em>metres</em>.
+     *
+     * @param  factory The factory to use for creating the transform.
+     * @return the map projection from (λ,φ) to (<var>x</var>,<var>y</var>) 
coordinates.
+     * @throws FactoryException if an error occurred while creating a 
transform.
+     */
+    @Override
+    public MathTransform createMapProjection(final MathTransformFactory 
factory) throws FactoryException {
+        if (isConic) {
+            return completeWithWraparound(factory);
+        } else {
+            return super.createMapProjection(factory);
+        }
+    }
+
     /**
      * Returns the names of additional internal parameters which need to be 
taken in account when
      * comparing two {@code SatelliteTracking} projections or formatting them 
in debug mode.
@@ -330,7 +341,6 @@ public class SatelliteTracking extends NormalizedProjection 
{
         double x = srcPts[srcOff];
         double y = λt - p2_on_p1 * λpm;
         if (isConic) {
-            x = wraparoundScaledLongitude(x, θ_bound);
             λpm = n*y + s0;                     // Use this variable for a new 
purpose. Needed for derivative.
             if ((Double.doubleToRawLongBits(λpm) ^ 
Double.doubleToRawLongBits(n)) < 0) {
                 /*
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
index 3f6ebf450e..4696faabb4 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
@@ -386,7 +386,7 @@ public class TransverseMercator extends 
NormalizedProjection {
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
         final Envelope2D domain = new Envelope2D();
         domain.x = -PI/2 * (40d/90);
-        domain.y = -PI/2 * (84d/90);
+        domain.y = -POLAR_AREA_LIMIT;
         domain.width  = -2 * domain.x;
         domain.height = -2 * domain.y;
         return Optional.of(domain);
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
index af043a9996..eff7676b22 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
@@ -160,7 +160,7 @@ public class ZonedGridSystem extends 
AbstractMathTransform2D implements Serializ
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        final double y = -PI/2 * (84d/90);
+        final double y = -NormalizedProjection.POLAR_AREA_LIMIT;
         return Optional.of(new Envelope2D(null, -PI, y, 2*PI, -2*y));
     }
 
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
index 0edb4c7586..42d0e846df 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
@@ -335,7 +335,7 @@ public final strictfp class LambertConicConformalTest 
extends MapProjectionTestC
         createCompleteProjection(new LambertConformal1SP(),
                 6371007,    // Semi-major axis length
                 6371007,    // Semi-minor axis length
-                0.5,        // Central meridian
+                0,          // Central meridian
                 40,         // Latitude of origin
                 NaN,        // Standard parallel 1
                 NaN,        // Standard parallel 2
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
index 15d09bdf6e..16bc487a69 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
@@ -364,7 +364,7 @@ public final strictfp class MercatorTest extends 
MapProjectionTestCase {
         createCompleteProjection(new Mercator1SP(),
                 6371007,    // Semi-major axis length
                 6371007,    // Semi-minor axis length
-                0.5,        // Central meridian
+                0,          // Central meridian
                 NaN,        // Latitude of origin (none)
                 NaN,        // Standard parallel 1 (none)
                 NaN,        // Standard parallel 2 (none)
@@ -408,5 +408,11 @@ public final strictfp class MercatorTest extends 
MapProjectionTestCase {
         isInverseTransformSupported = false;
         tolerance = Formulas.LINEAR_TOLERANCE;
         verifyTransform(coordinates, expected);
+        /*
+         * Replace the 181° longitude by -179° so we can test inverse 
projection.
+         */
+        coordinates[2] = -179;
+        isInverseTransformSupported = true;
+        verifyTransform(coordinates, expected);
     }
 }
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
index 4d99553d93..b7d56a93b2 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
@@ -170,7 +170,7 @@ public final strictfp class PolarStereographicTest extends 
MapProjectionTestCase
         createCompleteProjection(new PolarStereographicA(),
                 6371007,            // Semi-major axis length
                 6371007,            // Semi-minor axis length
-                0.5,                // Central meridian
+                0,                  // Central meridian
                 latitudeOfOrigin,   // Latitude of origin
                 NaN,                // Standard parallel 1 (none)
                 NaN,                // Standard parallel 2 (none)
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
index d512b4f971..3268687771 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
@@ -146,7 +146,7 @@ public final strictfp class TransverseMercatorTest extends 
MapProjectionTestCase
         createCompleteProjection(new 
org.apache.sis.internal.referencing.provider.TransverseMercator(),
                 6371007,    // Semi-major axis length
                 6371007,    // Semi-minor axis length
-                0.5,        // Central meridian
+                0,          // Central meridian
                 2.5,        // Latitude of origin
                 NaN,        // Standard parallel 1 (none)
                 NaN,        // Standard parallel 2 (none)

Reply via email to