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); + } }