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

Reply via email to