You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/02/06 21:55:21 UTC

[sis] branch geoapi-4.0 updated (865ea92 -> 991733b)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 865ea92  Bug fix: `PointOutsideCoverageException` was not correctly made silent when point is outside coverage.
     new 5959330  Avoid creating inverse transform when not needed.
     new 991733b  Change the way "North pole rotation" is created and change the way the inverse operation is created. The north pole case is now implemented as a "South pole rotation" operation rotating the antipodal point. It causes a 180° offset in longitude, but this is intended according the following definition from COSMO:

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../referencing/provider/NorthPoleRotation.java    |   9 +-
 .../referencing/provider/SouthPoleRotation.java    |   1 +
 .../operation/transform/AbstractMathTransform.java |   6 +-
 .../operation/transform/PoleRotation.java          | 221 +++++++++++++++------
 .../operation/transform/PoleRotationTest.java      | 129 +++++++++---
 5 files changed, 269 insertions(+), 97 deletions(-)

[sis] 01/02: Avoid creating inverse transform when not needed.

Posted by de...@apache.org.
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 5959330e5f2a7ab42c1824b018729e23c142a595
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Feb 5 18:31:55 2022 +0100

    Avoid creating inverse transform when not needed.
---
 .../sis/referencing/operation/transform/AbstractMathTransform.java  | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
index fa74432..27f051d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
@@ -78,7 +78,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
  * running the same SIS version.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.2
  *
  * @see DefaultMathTransformFactory
  * @see org.apache.sis.referencing.operation.AbstractCoordinateOperation
@@ -805,6 +805,10 @@ public abstract class AbstractMathTransform extends FormattableObject
         {
             return false;
         }
+        if ((tr1 instanceof LinearTransform) != (tr2 instanceof LinearTransform)) {
+            // For avoiding creation of inverse transform for a result that should be false.
+            return false;
+        }
         try {
             tr1 = tr1.inverse();
         } catch (NoninvertibleTransformException e) {

[sis] 02/02: Change the way "North pole rotation" is created and change the way the inverse operation is created. The north pole case is now implemented as a "South pole rotation" operation rotating the antipodal point. It causes a 180° offset in longitude, but this is intended according the following definition from COSMO:

Posted by de...@apache.org.
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 991733b1c3bd0d6268029392ecd71a8c50a69bc0
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Feb 6 22:47:41 2022 +0100

    Change the way "North pole rotation" is created and change the way the inverse operation is created.
    The north pole case is now implemented as a "South pole rotation" operation rotating the antipodal point.
    It causes a 180° offset in longitude, but this is intended according the following definition from COSMO:
    
    > It is convenient to define the rotated meridian which runs through both the geographical and the rotated North Pole as the 0◦ meridian.
    
    In addition, the way to create inverse operation has been modified in order to keep latitude in the range of valid values.
    Mathematically, it is sometime convenient to use an out-of-range value as a trick for avoiding the 180° offset in longitudes,
    and sometime it is convenient to keep the offset. We select whatever method keep longitude outputs closer to [-180 … 180]° range.
---
 .../referencing/provider/NorthPoleRotation.java    |   9 +-
 .../referencing/provider/SouthPoleRotation.java    |   1 +
 .../operation/transform/PoleRotation.java          | 221 +++++++++++++++------
 .../operation/transform/PoleRotationTest.java      | 129 +++++++++---
 4 files changed, 264 insertions(+), 96 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java
index 12e7f7d..a1ca56f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java
@@ -34,13 +34,8 @@ import org.apache.sis.measure.Units;
 
 /**
  * The provider for the NetCDF <cite>Rotated Latitude/Longitude</cite> coordinate operation.
- * This is similar to the WMO Rotated Latitude/Longitude but rotating north pole instead of
- * south pole.
- *
- * <h2>Comparison with UCAR library</h2>
- * {@link ucar.unidata.geoloc.projection.RotatedPole} in UCAR netCDF library version 5.5.2
- * gives results with an offset of 180° in longitude values compared to our implementation.
- * See {@code RotatedPoleTest.testRotateNorthPoleOnGreenwich()} for more details.
+ * This is similar to the WMO Rotated Latitude/Longitude but rotating north pole instead of south pole.
+ * The 0° rotated meridian is defined as the meridian that runs through both the geographical and the rotated North pole.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java
index 6e3fc84..bd737b5 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java
@@ -35,6 +35,7 @@ import org.apache.sis.measure.Units;
 /**
  * The provider for the WMO <cite>Rotated Latitude/Longitude</cite> coordinate operation.
  * This is defined by the World Meteorological Organization (WMO) in GRIB2 template 3.1.
+ * The 180° rotated meridian runs through both the geographical and the rotated South pole.
  *
  * <h2>Comparison with UCAR library</h2>
  * This is consistent with {@link ucar.unidata.geoloc.projection.RotatedLatLon}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PoleRotation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PoleRotation.java
index 0674050..cba2077 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PoleRotation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PoleRotation.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
-import java.util.List;
 import java.util.Collections;
 import java.io.Serializable;
 import org.opengis.util.FactoryException;
@@ -25,7 +24,6 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform2D;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
-import org.opengis.parameter.GeneralParameterValue;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
@@ -82,6 +80,20 @@ public class PoleRotation extends AbstractMathTransform2D implements Serializabl
     private static final long serialVersionUID = -8355693495724373931L;
 
     /**
+     * Index of parameter declared in {@link SouthPoleRotation} and {@link NorthPoleRotation}.
+     *
+     * @see #setValue(int, double)
+     */
+    private static final int POLE_LATITUDE = 0, POLE_LONGITUDE = 1, AXIS_ANGLE = 2;
+
+    /**
+     * The maximal value of axis rotation before switching to a different algorithm which will
+     * reduce that rotation. The intent it to have axis rotation (applied on longitude values)
+     * small enough for increasing the chances that output longitudes are in [-180 … 180]° range.
+     */
+    private static final double MAX_AXIS_ROTATION = 90;
+
+    /**
      * The parameters used for creating this transform.
      * They are used for formatting <cite>Well Known Text</cite> (WKT).
      *
@@ -119,8 +131,14 @@ public class PoleRotation extends AbstractMathTransform2D implements Serializabl
     private MathTransform2D inverse;
 
     /**
-     * Creates the inverse of the given forward operation.
-     * The new pole latitude is φ<sub>p</sub> = (180° − φ<sub>forward</sub>).
+     * Creates the inverse of the given forward operation. In principle, the latitude φ<sub>p</sub>
+     * should be unchanged and the longitude λ<sub>p</sub> should be 180° (ignoring the axis angle)
+     * in order to go back in the direction of geographical South pole. The longitudes computed by
+     * this approach have an offset of 180°, which can be compensated with the axis angle (see the
+     * {@link #inverseParameter(Parameters, ParameterValue)} method for more details).
+     *
+     * However we can get a mathematically equivalent effect without the 180° longitude offset by
+     * setting the new pole latitude to unrealistic φ<sub>p</sub> = (180° − φ<sub>forward</sub>) value.
      * We get this effect be inverting the sign of {@link #cosφp} while keeping {@link #sinφp} unchanged.
      * Note that this is compatible with {@link #isIdentity()} implementation.
      *
@@ -134,37 +152,71 @@ public class PoleRotation extends AbstractMathTransform2D implements Serializabl
     }
 
     /**
-     * Computes the value of the given parameter for the inverse operation.
-     * This method is invoked for each parameter.
+     * Computes the value of the given parameter for the inverse of "South pole rotation".
+     * This method is invoked for each parameter of the inverse transform to initialize.
+     * The parameters of the inverse transform is defined as below:
+     *
+     * <ul>
+     *   <li><b>Latitude</b> is unchanged. For example if the rotated pole was located at 60° of latitude
+     *       relative to the geographic pole, then conversely the geographic pole is still located at 60°
+     *       of latitude relative to the rotated pole.</li>
+     *   <li><b>Longitude</b> is 180° (ignoring axis rotation) in the South pole case because by definition
+     *       the 180° rotated meridian runs through both the geographical and the rotated South pole.</li>
+     *   <li><b>Axis rotation</b> is 180° (ignoring λ<sub>p</sub> in forward transform) in the South pole
+     *       case for compensating the 180° offset of λ<sub>p</sub> in the inverse transform.</li>
+     *   <li>If a non-zero λ<sub>p</sub> was specified in the forward transform,
+     *       then an axis rotation in opposite direction must be added to the inverse transform.
+     *       Conversely if an axis rotation was defined in the forward transform,
+     *       then a λ<sub>p</sub> rotation in opposite direction must be added to the inverse transform.</li>
+     * </ul>
      *
      * @param  forward  the forward operation.
      * @param  target   parameter to initialize.
      * @return whether to accept the parameter (always {@code true}).
+     *
+     * @see #inverse()
      */
     private static boolean inverseParameter(final Parameters forward, final ParameterValue<?> target) {
-        final ParameterDescriptor<?> descriptor = target.getDescriptor();
-        final List<GeneralParameterValue> values = forward.values();
-        for (int i = values.size(); --i >= 0;) {
-            if (descriptor.equals(values.get(i).getDescriptor())) {
-                if (i != 0) {
-                    /*
-                     * For assigning a value to the "grid_south_pole_longitude" parameter at index 1,
-                     * we derive the value from the "grid_south_pole_angle" parameter at index 2.
-                     * And conversely.
-                     */
-                    i = 3 - i;
-                }
-                double value = ((Number) ((ParameterValue<?>) values.get(i)).getValue()).doubleValue();
-                if (i == 0) {
-                    value = IEEEremainder(180 - value, 360);
-                } else if (SouthPoleRotation.PARAMETERS.equals(forward.getDescriptor())) {
-                    value = -value;
-                }
-                target.setValue(value);
-                return true;
-            }
+        final ParameterDescriptorGroup descriptor = forward.getDescriptor();
+        int i = descriptor.descriptors().indexOf(target.getDescriptor());
+        if (i < 0) {
+            return false;                               // Should never happen.
         }
-        return false;       // Should never happen.
+        if (i != POLE_LATITUDE) {
+            /*
+             * For assigning a value to the "grid_south_pole_longitude" parameter at index 1,
+             * we derive the value from the "grid_south_pole_angle" parameter at index 2.
+             * And conversely.
+             */
+            i = (AXIS_ANGLE + POLE_LONGITUDE) - i;      // AXIS_ANGLE - (i - POLE_LONGITUDE)
+        }
+        Number value = getValue(forward, i);
+        if (i != POLE_LATITUDE && SouthPoleRotation.PARAMETERS.equals(descriptor)) {
+            double λp = value.doubleValue();
+            value = copySign(180, λp) - λp;             // Negative of antipodal longitude.
+        }
+        target.setValue(value);
+        return true;
+    }
+
+    /**
+     * Returns the value for the parameter at the given index.
+     * This is the converse of {@link #setValue(int, double)}.
+     */
+    private static Number getValue(final Parameters context, final int index) {
+        return ((Number) ((ParameterValue<?>) context.values().get(index)).getValue());
+    }
+
+    /**
+     * Sets the value of the parameter at the given index.
+     * In the rotated south pole case, parameter 0 to 2 (inclusive) are:
+     * {@code "grid_south_pole_latitude"},
+     * {@code "grid_south_pole_longitude"} and
+     * {@code "grid_south_pole_angle"} in that order.
+     */
+    private void setValue(final int index, final double value) {
+        final ParameterDescriptor<?> p = (ParameterDescriptor<?>) context.getDescriptor().descriptors().get(index);
+        context.parameter(p.getName().getCode()).setValue(value);
     }
 
     /**
@@ -175,77 +227,90 @@ public class PoleRotation extends AbstractMathTransform2D implements Serializabl
      * @param  south  {@code true} for a south pole rotation, or {@code false} for a north pole rotation.
      * @param  φp     geographic  latitude in degrees of the southern pole of the coordinate system.
      * @param  λp     geographic longitude in degrees of the southern pole of the coordinate system.
-     * @param  pa     angle of rotation in degrees about the new polar axis measured clockwise when
+     * @param  θp     angle of rotation in degrees about the new polar axis measured clockwise when
      *                looking from the rotated pole to the Earth center.
      */
-    protected PoleRotation(final boolean south, final double φp, final double λp, final double pa) {
+    protected PoleRotation(final boolean south, double φp, double λp, double θp) {
         context = new ContextualParameters(
                 south ? SouthPoleRotation.PARAMETERS
                       : NorthPoleRotation.PARAMETERS, DIMENSION, DIMENSION);
-        setValue(0, φp);        // grid_south_pole_latitude   or  grid_north_pole_latitude
-        setValue(1, λp);        // grid_south_pole_longitude  or  grid_north_pole_longitude
-        setValue(2, pa);        // grid_south_pole_angle      or  north_pole_grid_longitude
+        setValue(POLE_LATITUDE,  φp);       // grid_south_pole_latitude   or  grid_north_pole_latitude
+        setValue(POLE_LONGITUDE, λp);       // grid_south_pole_longitude  or  grid_north_pole_longitude
+        setValue(AXIS_ANGLE,     θp);       // grid_south_pole_angle      or  north_pole_grid_longitude
+        if (south) {
+            θp  = -θp;
+        } else {
+            φp  = -φp;
+            λp -= copySign(180, λp);        // Antipodal point.
+        }
+        double sign = 1;
+        if (abs(θp) > MAX_AXIS_ROTATION) {
+            /*
+             * Inverting the sign of sin(φp), cos(φp) and λ (in normalization matrix) will cause the formula to
+             * compute the antipodal point, which allows us to remove 180° from `θp` and make it closer to zero.
+             * Transform will produce final longitude results that are closer to the [-180 … +180]° range.
+             */
+            sign = -1;
+            θp -= copySign(180, θp);
+            context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION)  .convertAfter (0, -1, null);  // Invert λ sign.
+            context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION).convertBefore(1, -1, null);  // Invert φ sign.
+        }
         final double φ = toRadians(φp);
-        final double sign = south ? 1 : -1;
         sinφp = sin(φ) * sign;
         cosφp = cos(φ) * sign;
         context.normalizeGeographicInputs(λp);
-        context.denormalizeGeographicOutputs(south ? -pa : pa);
-    }
-
-    /**
-     * Sets the value of the parameter at the given index.
-     * In the rotated south pole case, parameter 0 to 2 (inclusive) are:
-     * {@code "grid_south_pole_latitude"},
-     * {@code "grid_south_pole_longitude"} and
-     * {@code "grid_south_pole_angle"} in that order.
-     */
-    private void setValue(final int index, final double value) {
-        final ParameterDescriptor<?> p = (ParameterDescriptor<?>) context.getDescriptor().descriptors().get(index);
-        context.parameter(p.getName().getCode()).setValue(value);
+        context.denormalizeGeographicOutputs(θp);
     }
 
     /**
-     * Creates a new rotated south pole operation.
+     * Creates a new rotated south pole operation. The rotations are applied by first rotating the sphere
+     * through λ<sub>p</sub> about the geographic polar axis, then rotating through (φ<sub>p</sub> − (−90°))
+     * degrees so that the southern pole moved along the (previously rotated) Greenwich meridian,
+     * and finally by rotating clockwise when looking from the southern to the northern rotated pole.
+     * The 180° rotated meridian runs through both the geographical and the rotated South pole.
      *
      * @param  factory  the factory to use for creating the transform.
      * @param  φp       geographic  latitude in degrees of the southern pole of the coordinate system.
      * @param  λp       geographic longitude in degrees of the southern pole of the coordinate system.
-     * @param  pa       angle of rotation in degrees about the new polar axis measured clockwise when
+     * @param  θp       angle of rotation in degrees about the new polar axis measured clockwise when
      *                  looking from the southern to the northern pole.
      * @return the conversion doing a south pole rotation.
      * @throws FactoryException if an error occurred while creating a transform.
      */
     public static MathTransform rotateSouthPole(final MathTransformFactory factory,
-            final double φp, final double λp, final double pa) throws FactoryException
+            final double φp, final double λp, final double θp) throws FactoryException
     {
-        final PoleRotation kernel = new PoleRotation(true, φp, λp, pa);
+        final PoleRotation kernel = new PoleRotation(true, φp, λp, θp);
         return kernel.context.completeTransform(factory, kernel);
     }
 
     /**
-     * Creates a new rotated north pole operation.
+     * Creates a new rotated north pole operation. The rotations are applied by first rotating the sphere
+     * through λ<sub>p</sub> about the geographic polar axis, then rotating through (φ<sub>p</sub> − 90°)
+     * degrees so that the northern pole moved along the (previously rotated) Greenwich meridian.
+     * The 0° rotated meridian is defined as the meridian that runs through both the geographical and the
+     * rotated North pole.
      *
      * @param  factory  the factory to use for creating the transform.
      * @param  φp       geographic  latitude in degrees of the northern pole of the coordinate system.
      * @param  λp       geographic longitude in degrees of the northern pole of the coordinate system.
-     * @param  pa       angle of rotation in degrees about the new polar axis measured clockwise when
+     * @param  θp       angle of rotation in degrees about the new polar axis measured clockwise when
      *                  looking from the northern to the southern pole.
      * @return the conversion doing a north pole rotation.
      * @throws FactoryException if an error occurred while creating a transform.
      *
-     * @todo Current implementation does not accept non-zero {@code pa} argument value,
+     * @todo Current implementation does not accept non-zero {@code θp} argument value,
      *       because we have not yet resolved an ambiguity about the sign of this parameter.
      *       Should it be a rotation clockwise or anti-clockwise? Looking from northern to
      *       southern pole or the opposite direction?
      */
     public static MathTransform rotateNorthPole(final MathTransformFactory factory,
-            final double φp, final double λp, final double pa) throws FactoryException
+            final double φp, final double λp, final double θp) throws FactoryException
     {
-        if (pa != 0) {
+        if (θp != 0) {
             throw new IllegalArgumentException("Non-zero axis rotation not yet accepted.");
         }
-        final PoleRotation kernel = new PoleRotation(false, φp, λp, pa);
+        final PoleRotation kernel = new PoleRotation(false, φp, λp, θp);
         return kernel.context.completeTransform(factory, kernel);
     }
 
@@ -395,12 +460,52 @@ public class PoleRotation extends AbstractMathTransform2D implements Serializabl
     @Override
     public synchronized MathTransform2D inverse() {
         if (inverse == null) {
-            inverse = new PoleRotation(this);
+            final PoleRotation simple = new PoleRotation(this);
+            final ContextualParameters inverseParameters = simple.context;
+            final double θp = inverseParameters.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION).getElement(0, 2);
+            if (abs(θp) > MAX_AXIS_ROTATION) {
+                /*
+                 * If the θp value added to output longitude values is greater than 90°,
+                 * create an alternative operation which will keep that value below 90°.
+                 * The intent is to keep output λ closer to the [-180 … +180]° range.
+                 */
+                final PoleRotation alternative = new PoleRotation(
+                        SouthPoleRotation.PARAMETERS.equals(inverseParameters.getDescriptor()),
+                        getValue(inverseParameters, POLE_LATITUDE).doubleValue(),
+                        getValue(inverseParameters, POLE_LONGITUDE).doubleValue(),
+                        getValue(inverseParameters, AXIS_ANGLE).doubleValue());     // Not necessarily equals to θp.
+                /*
+                 * The caller of this method expects a chain of operations where a normalization is applied before
+                 * the pole rotation, and a denormalization is applied after. Those expected (de)normalization are
+                 * specified by `inverse.context`. But the actual normalization and denormalization needed by the
+                 * alternative pole rotation are a little bit different. So we need to cancel the old normalization
+                 * before to apply the new one, and to cancel the old denormalization after we applied to new one.
+                 */
+                final ContextualParameters actualParameters = alternative.context;
+                inverse = MathTransforms.concatenate(
+                        concatenate(inverseParameters, ContextualParameters.MatrixRole.INVERSE_NORMALIZATION,
+                                    actualParameters,  ContextualParameters.MatrixRole.NORMALIZATION),
+                        alternative,
+                        concatenate(actualParameters,  ContextualParameters.MatrixRole.DENORMALIZATION,
+                                    inverseParameters, ContextualParameters.MatrixRole.INVERSE_DENORMALIZATION));
+            } else {
+                inverse = simple;
+            }
         }
         return inverse;
     }
 
     /**
+     * Returns the concatenation of transform {@code p1.r1} followed by {@code p2.r2}.
+     */
+    private static MathTransform2D concatenate(
+            final ContextualParameters p1, final ContextualParameters.MatrixRole r1,
+            final ContextualParameters p2, final ContextualParameters.MatrixRole r2)
+    {
+        return (MathTransform2D) MathTransforms.linear(p2.getMatrix(r2).multiply(p1.getMatrix(r1)));
+    }
+
+    /**
      * Tests whether this transform does not move any points.
      *
      * @return {@code true} if this transform is (at least approximately) the identity transform.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PoleRotationTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PoleRotationTest.java
index 15bb190..bb0525e 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PoleRotationTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PoleRotationTest.java
@@ -80,8 +80,8 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
 
     /**
      * Tests a rotation of south pole with the new pole on Greenwich.
-     * The {@link ucar.unidata.geoloc.projection.RotatedLatLon} class
-     * has been used as a reference implementation for expected values.
+     * The {@link ucar.unidata.geoloc.projection.RotatedLatLon} class has
+     * been used as a reference implementation for computing expected values.
      *
      * @throws FactoryException if the transform can not be created.
      * @throws TransformException if an error occurred while transforming a point.
@@ -95,9 +95,36 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
             100, -61
         };
         final double[] expected = {         // (λ,φ) coordinates after conversion.
-              0.000000000, -81.000000000,
-             60.140453893, -75.629715301,
-            136.900518716, -45.671868261
+              0,               -81,
+             60.1404538930820, -75.6297153018960,
+            136.9005187159727, -45.6718682605614
+        };
+        verifyTransform(coordinates, expected);
+        inverseSouthPoleTransform();
+        verifyTransform(expected, coordinates);
+    }
+
+    /**
+     * Tests a rotation of south pole with the new pole on a non-zero longitude.
+     * The {@link ucar.unidata.geoloc.projection.RotatedLatLon} class has been
+     * used as a reference implementation for computing expected values.
+     *
+     * @throws FactoryException if the transform can not be created.
+     * @throws TransformException if an error occurred while transforming a point.
+     */
+    @Test
+    @DependsOnMethod("testRotateSouthPoleOnGreenwich")
+    public void testRotateSouthPoleOnOtherLongitude() throws FactoryException, TransformException {
+        transform = PoleRotation.rotateSouthPole(factory(), -70, 25, 0);
+        final double[] coordinates = {      // (λ,φ) coordinates to convert.
+             25, -69,
+             20, -51,
+            100, -71
+        };
+        final double[] expected = {         // (λ,φ) coordinates after conversion.
+              0,               -89,
+             -9.6282124673448, -70.8563796930179,
+            127.8310735055447, -66.5368804564497
         };
         verifyTransform(coordinates, expected);
         inverseSouthPoleTransform();
@@ -107,13 +134,13 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
     /**
      * Tests a rotation of south pole with the pole on arbitrary meridian.
      * The {@link ucar.unidata.geoloc.projection.RotatedLatLon} class has
-     * been used as a reference implementation for expected values.
+     * been used as a reference implementation for computing expected values.
      *
      * @throws FactoryException if the transform can not be created.
      * @throws TransformException if an error occurred while transforming a point.
      */
     @Test
-    @DependsOnMethod("testRotateSouthPoleOnGreenwich")
+    @DependsOnMethod("testRotateSouthPoleOnOtherLongitude")
     public void testRotateSouthPoleWithAngle() throws FactoryException, TransformException {
         transform = PoleRotation.rotateSouthPole(factory(), -50, 20, 10);
         final double[] coordinates = {      // (λ,φ) coordinates to convert.
@@ -122,9 +149,9 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
             -30, -89
         };
         final double[] expected = {         // (λ,φ) coordinates after conversion.
-             170.000000000, -89.000000000,
-              95.348788748, -49.758697265,
-            -188.792151374, -50.636582758
+             170,               -89,
+              95.3487887483185, -49.7586972646198,
+            -188.7921513735695, -50.6365827575445
         };
         verifyTransform(coordinates, expected);
         inverseSouthPoleTransform();
@@ -133,11 +160,14 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
 
     /**
      * Tries rotating a pole to opposite hemisphere.
+     * The {@link ucar.unidata.geoloc.projection.RotatedLatLon} class has
+     * been used as a reference implementation for computing expected values.
      *
      * @throws FactoryException if the transform can not be created.
      * @throws TransformException if an error occurred while transforming a point.
      */
     @Test
+    @DependsOnMethod("testRotateSouthPoleWithAngle")
     public void testRotateSouthToOppositeHemisphere() throws FactoryException, TransformException {
         transform = PoleRotation.rotateSouthPole(factory(), 50, 20, 10);
         final double[] coordinates = {      // (λ,φ) coordinates to convert.
@@ -146,9 +176,9 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
             -30, 89
         };
         final double[] expected = {         // (λ,φ) coordinates after conversion.
-             -10.000000000, -89.000000000,
-              64.651211252, -49.758697265,
-             -11.207848626, -50.636582758
+             -10,               -89,
+              64.6512112516815, -49.7586972646198,
+             -11.2078486264305, -50.6365827575445
         };
         verifyTransform(coordinates, expected);
         inverseSouthPoleTransform();
@@ -157,13 +187,8 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
 
     /**
      * Tests a rotation of north pole with the new pole on Greenwich.
-     *
-     * <h4>Comparison with UCAR library</h4>
-     * {@link ucar.unidata.geoloc.projection.RotatedPole} in UCAR netCDF library version 5.5.2
-     * gives results with an offset of 180° in longitude values compared to our implementation.
-     * But geometrical reasoning suggests that our implementation is correct: if we rotate the
-     * pole to 60°N, then latitude of 54°N on Greenwich meridian become only 6° below new pole,
-     * i.e. 84°N but still on the same meridian (Greenwich) because we did not cross the pole.
+     * The {@link ucar.unidata.geoloc.projection.RotatedPole} class
+     * has been used as a reference implementation for expected values.
      *
      * @throws FactoryException if the transform can not be created.
      * @throws TransformException if an error occurred while transforming a point.
@@ -177,9 +202,36 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
            -30, 89
         };
         final double[] expected = {         // (λ,φ) coordinates after conversion.
-               0.000000000, 84.000000000,
-             110.307140436, 80.141810970,
-            -178.973119126, 60.862133738
+            180, 84,
+            -69.6928595614074,  80.1418109704940,
+              1.0268808754468,  60.8621337379806
+        };
+        verifyTransform(coordinates, expected);
+        inverseNorthPoleTransform();
+        verifyTransform(expected, coordinates);
+    }
+
+    /**
+     * Tests a rotation of north pole with the new pole on a non-zero longitude.
+     * The {@link ucar.unidata.geoloc.projection.RotatedPole} class has been used
+     * as a reference implementation for computing expected values.
+     *
+     * @throws FactoryException if the transform can not be created.
+     * @throws TransformException if an error occurred while transforming a point.
+     */
+    @Test
+    @DependsOnMethod("testRotateNorthPoleOnGreenwich")
+    public void testRotateNorthPoleOnOtherLongitude() throws FactoryException, TransformException {
+        transform = PoleRotation.rotateNorthPole(factory(), 70, 25, 0);
+        final double[] coordinates = {      // (λ,φ) coordinates to convert.
+             25, 72,
+             20, 51,
+            100, 71
+        };
+        final double[] expected = {         // (λ,φ) coordinates after conversion.
+              0,                88,
+            170.3717875326552,  70.8563796930179,
+            -52.1689264944553,  66.5368804564497
         };
         verifyTransform(coordinates, expected);
         inverseNorthPoleTransform();
@@ -219,9 +271,9 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
            -30, 89
         };
         final double[] expected = {         // (λ,φ) coordinates after conversion.
-             -58.817428350, 66.096411904,
-             -44.967324181, 78.691210976,
-            -167.208632734, 70.320491507
+             121.1825716500646,  66.0964119035041,
+             135.0326758188633,  78.6912109761956,
+              12.7913672657394,  70.3204915065785
         };
         verifyTransform(coordinates, expected);
         inverseNorthPoleTransform();
@@ -243,9 +295,9 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
             -30, -89
         };
         final double[] expected = {         // (λ,φ) coordinates after conversion.
-             -10.000000000, 89.000000000,
-              64.651211252, 49.758697265,
-             -11.207848626, 50.636582758
+             170,                89,
+            -115.3487887483185,  49.7586972646198,
+             168.7921513735695,  50.6365827575445
         };
         verifyTransform(coordinates, expected);
         inverseNorthPoleTransform();
@@ -253,17 +305,32 @@ public final strictfp class PoleRotationTest extends MathTransformTestCase {
     }
 
     /**
-     * Tests derivative.
+     * Tests derivative for a south pole rotation.
      *
      * @throws FactoryException if the transform can not be created.
      * @throws TransformException if an error occurred while computing a derivative.
      */
     @Test
-    public void testDerivative() throws FactoryException, TransformException {
+    public void testDerivativeSouth() throws FactoryException, TransformException {
         transform = PoleRotation.rotateSouthPole(factory(), -50, 0, 0);
         derivativeDeltas = new double[] {1E-6, 1E-6};
         verifyDerivative(  0, -51);
         verifyDerivative( 20, -58);
         verifyDerivative(-30, -40);
     }
+
+    /**
+     * Tests derivative for a north pole rotation.
+     *
+     * @throws FactoryException if the transform can not be created.
+     * @throws TransformException if an error occurred while computing a derivative.
+     */
+    @Test
+    public void testDerivativeNorth() throws FactoryException, TransformException {
+        transform = PoleRotation.rotateNorthPole(factory(), 50, 0, 0);
+        derivativeDeltas = new double[] {1E-5, 1E-5};
+        verifyDerivative(  0, 51);
+        verifyDerivative( 20, 58);
+        verifyDerivative(-30, 40);
+    }
 }