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 33d5d46b7cbcdb392c6a2c140b204f708bbb6389
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Jan 24 12:06:56 2022 +0100

    Add "Rotated Latitude/Longitude" coordinate operation.
---
 .../referencing/provider/RotatedNorthPole.java     | 150 +++++++++
 .../referencing/provider/RotatedSouthPole.java     | 154 +++++++++
 .../referencing/provider/package-info.java         |   2 +-
 .../operation/transform/RotatedPole.java           | 357 +++++++++++++++++++++
 ...g.opengis.referencing.operation.OperationMethod |   2 +
 .../referencing/provider/ProvidersTest.java        |   4 +-
 .../operation/transform/RotatedPoleTest.java       | 118 +++++++
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 8 files changed, 786 insertions(+), 2 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RotatedNorthPole.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RotatedNorthPole.java
new file mode 100644
index 0000000..22b2ae9
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RotatedNorthPole.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing.provider;
+
+import javax.xml.bind.annotation.XmlTransient;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.apache.sis.referencing.operation.transform.RotatedPole;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.measure.Longitude;
+import org.apache.sis.measure.Latitude;
+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.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ *
+ * @see <a 
href="https://cfconventions.org/cf-conventions/cf-conventions.html#_rotated_pole";>Rotated
 pole in CF-conversions</a>
+ *
+ * @since 1.2
+ * @module
+ */
+@XmlTransient
+public final class RotatedNorthPole extends AbstractProvider {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 3485083285768740448L;
+
+    /**
+     * The operation parameter descriptor for the <cite>grid north pole 
latitude</cite> parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> NetCDF:  </td><td> grid_north_pole_latitude </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> GRID_POLE_LATITUDE;
+
+    /**
+     * The operation parameter descriptor for the <cite>grid north pole 
longitude</cite> parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> NetCDF:  </td><td> grid_north_pole_longitude </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> GRID_POLE_LONGITUDE;
+
+    /**
+     * The operation parameter descriptor for the 
<cite>north_pole_grid_longitude</cite> parameter value.
+     * This parameter is optional.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> NetCDF:  </td><td> north_pole_grid_longitude </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>Value domain: [-180.0 … 180.0]°</li>
+     *   <li>Optional</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> GRID_POLE_ANGLE;
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    public static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        final ParameterBuilder builder = new 
ParameterBuilder().setCodeSpace(Citations.NETCDF, "NetCDF").setRequired(true);
+
+        GRID_POLE_LATITUDE = builder.addName("grid_north_pole_latitude")
+                .createBounded(Latitude.MIN_VALUE, Latitude.MAX_VALUE, 
Double.NaN, Units.DEGREE);
+
+        GRID_POLE_LONGITUDE = builder.addName("grid_north_pole_longitude")
+                .createBounded(Longitude.MIN_VALUE, Longitude.MAX_VALUE, 
Double.NaN, Units.DEGREE);
+
+        GRID_POLE_ANGLE = 
builder.setRequired(false).addName("north_pole_grid_longitude")
+                .createBounded(Longitude.MIN_VALUE, Longitude.MAX_VALUE, 0, 
Units.DEGREE);
+
+        PARAMETERS = builder.setRequired(true)
+                .addName("rotated_latitude_longitude")
+                .createGroup(GRID_POLE_LATITUDE,    // Note: `RotatedPole` 
implementation depends on this parameter order.
+                             GRID_POLE_LONGITUDE,
+                             GRID_POLE_ANGLE);
+    }
+
+    /**
+     * Constructs a new provider.
+     */
+    public RotatedNorthPole() {
+        super(2, 2, PARAMETERS);
+    }
+
+    /**
+     * Creates a coordinate operation from the specified group of parameter 
values.
+     *
+     * @param  factory     the factory to use for creating the transforms.
+     * @param  parameters  the group of parameter values.
+     * @return the coordinate operation created from the given parameter 
values.
+     * @throws FactoryException if the coordinate operation can not be created.
+     */
+    @Override
+    public MathTransform createMathTransform(final MathTransformFactory 
factory, final ParameterValueGroup parameters)
+            throws FactoryException
+    {
+        final Parameters p = Parameters.castOrWrap(parameters);
+        return RotatedPole.rotateNorthPole(factory,
+                p.getValue(GRID_POLE_LONGITUDE),
+                p.getValue(GRID_POLE_LATITUDE),
+                p.getValue(GRID_POLE_ANGLE));
+    }
+}
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RotatedSouthPole.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RotatedSouthPole.java
new file mode 100644
index 0000000..734e4b2
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/RotatedSouthPole.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing.provider;
+
+import javax.xml.bind.annotation.XmlTransient;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.apache.sis.referencing.operation.transform.RotatedPole;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.measure.Longitude;
+import org.apache.sis.measure.Latitude;
+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.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+@XmlTransient
+public final class RotatedSouthPole extends AbstractProvider {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -5970630604222205521L;
+
+    /**
+     * The operation parameter descriptor for the <cite>grid south pole 
latitude</cite> parameter value.
+     * This is the geographic latitude (usually in degrees) of the southern 
pole of the coordinate system.
+     * The symbol used in GRIB2 template 3.1 is θ<sub>p</sub>.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> NetCDF:  </td><td> grid_south_pole_latitude </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> GRID_POLE_LATITUDE;
+
+    /**
+     * The operation parameter descriptor for the <cite>grid south pole 
longitude</cite> parameter value.
+     * This is the geographic longitude (usually in degrees) of the southern 
pole of the coordinate system.
+     * The symbol used in GRIB2 template 3.1 is λ<sub>p</sub>.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> NetCDF:  </td><td> grid_south_pole_longitude </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> GRID_POLE_LONGITUDE;
+
+    /**
+     * The operation parameter descriptor for the 
<cite>grid_south_pole_angle</cite> parameter value (optional).
+     * This is the angle of rotation about the new polar axis (measured 
clockwise when looking from the southern
+     * to the northern pole) of the coordinate system, assuming the new axis 
to have been obtained by first
+     * rotating the sphere through λ<sub>p</sub> about the geographic polar 
axis, and then rotating through
+     * (90° + θ<sub>p</sub>) degrees so that the southern pole moved along the 
(previously rotated) Greenwich meridian.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> NetCDF:  </td><td> grid_south_pole_angle </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>Value domain: [-180.0 … 180.0]°</li>
+     *   <li>Optional</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> GRID_POLE_ANGLE;
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    public static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        final ParameterBuilder builder = new 
ParameterBuilder().setCodeSpace(Citations.NETCDF, "NetCDF").setRequired(true);
+
+        GRID_POLE_LATITUDE = builder.addName("grid_south_pole_latitude")
+                .createBounded(Latitude.MIN_VALUE, Latitude.MAX_VALUE, 
Double.NaN, Units.DEGREE);
+
+        GRID_POLE_LONGITUDE = builder.addName("grid_south_pole_longitude")
+                .createBounded(Longitude.MIN_VALUE, Longitude.MAX_VALUE, 
Double.NaN, Units.DEGREE);
+
+        GRID_POLE_ANGLE = 
builder.setRequired(false).addName("grid_south_pole_angle")
+                .createBounded(Longitude.MIN_VALUE, Longitude.MAX_VALUE, 0, 
Units.DEGREE);
+
+        PARAMETERS = builder.setRequired(true)
+                .addName(Citations.WMO, "Rotated Latitude/longitude")
+                .addName("rotated_latlon_grib")
+                .createGroup(GRID_POLE_LATITUDE,    // Note: `RotatedPole` 
implementation depends on this parameter order.
+                             GRID_POLE_LONGITUDE,
+                             GRID_POLE_ANGLE);
+    }
+
+    /**
+     * Constructs a new provider.
+     */
+    public RotatedSouthPole() {
+        super(2, 2, PARAMETERS);
+    }
+
+    /**
+     * Creates a coordinate operation from the specified group of parameter 
values.
+     *
+     * @param  factory     the factory to use for creating the transforms.
+     * @param  parameters  the group of parameter values.
+     * @return the coordinate operation created from the given parameter 
values.
+     * @throws FactoryException if the coordinate operation can not be created.
+     */
+    @Override
+    public MathTransform createMathTransform(final MathTransformFactory 
factory, final ParameterValueGroup parameters)
+            throws FactoryException
+    {
+        final Parameters p = Parameters.castOrWrap(parameters);
+        return RotatedPole.rotateSouthPole(factory,
+                p.getValue(GRID_POLE_LONGITUDE),
+                p.getValue(GRID_POLE_LATITUDE),
+                p.getValue(GRID_POLE_ANGLE));
+    }
+}
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
index ecc9b95..466e271 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
@@ -22,7 +22,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Matthieu Bastianelli (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see org.apache.sis.referencing.operation.transform.MathTransformProvider
  *
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/RotatedPole.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/RotatedPole.java
new file mode 100644
index 0000000..5b19036
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/RotatedPole.java
@@ -0,0 +1,357 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.transform;
+
+import java.util.List;
+import java.util.Collections;
+import java.io.Serializable;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.Matrix;
+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;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.internal.referencing.provider.RotatedNorthPole;
+import org.apache.sis.internal.referencing.provider.RotatedSouthPole;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.ImmutableIdentifier;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.Debug;
+
+import static java.lang.Math.*;
+
+
+/**
+ * Computes latitudes and longitudes on a sphere where the south pole has been 
moved to given geographic coordinates.
+ * The parameter values of this transform use the conventions defined in 
template 3.1 of GRIB2 format published by the
+ * <a href="https://www.wmo.int/";>World Meteorological Organization</a> (WMO):
+ *
+ * <ol>
+ *   <li><b>λ<sub>p</sub>:</b> geographic longitude in degrees of the southern 
pole of the coordinate system.</li>
+ *   <li><b>θ<sub>p</sub>:</b> geographic  latitude in degrees of the southern 
pole of the coordinate system.</li>
+ *   <li>Angle of rotation in degrees about the new polar axis measured 
clockwise when looking from the southern
+ *       to the northern pole.</li>
+ * </ol>
+ *
+ * The rotations are applied by first rotating the sphere through 
λ<sub>p</sub> about the geographic polar axis,
+ * and then rotating through (θ<sub>p</sub> − (−90°)) degrees so that the 
southern pole moved along the
+ * (previously rotated) Greenwich meridian.
+ *
+ * <p>Source and target axis order is (<var>longitude</var>, 
<var>latitude</var>).
+ * This is the usual axis order used by Apache SIS for <em>internal</em> 
calculations.
+ * If a different axis order is desired (for example for showing coordinates 
to the user),
+ * an affine transform can be concatenated to this transform.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public class RotatedPole extends AbstractMathTransform2D implements 
Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -8355693495724373931L;
+
+    /**
+     * The parameters used for creating this transform.
+     * They are used for formatting <cite>Well Known Text</cite> (WKT).
+     *
+     * @see #getContextualParameters()
+     */
+    private final ContextualParameters context;
+
+    /**
+     * Sine and cosine of the geographic latitude of the southern pole of the 
coordinate system.
+     * The rotation angle to apply is (θ<sub>p</sub> − (−90°)) degrees for the 
south pole (−90°),
+     * but we use the following trigonometric identities:
+     *
+     * <p>For the south pole:</p>
+     * <ul>
+     *   <li>sin(θ + 90°) =  cos(θ)</li>
+     *   <li>cos(θ + 90°) = −sin(θ)</li>
+     * </ul>
+     *
+     * <p>For the north pole:</p>
+     * <ul>
+     *   <li>sin(θ − 90°) = −cos(θ)</li>
+     *   <li>cos(θ − 90°) =  sin(θ)</li>
+     * </ul>
+     *
+     * By convention those fields contain the sine and cosine for the south 
pole case,
+     * and values with opposite sign for the north pole case.
+     */
+    private final double sinθp, cosθp;
+
+    /**
+     * The inverse of this operation, computed when first needed.
+     *
+     * @see #inverse()
+     */
+    private MathTransform2D inverse;
+
+    /**
+     * Creates the inverse of the given forward operation.
+     *
+     * @see #inverse()
+     */
+    private RotatedPole(final RotatedPole forward) {
+        context =  forward.context.inverse(forward.context.getDescriptor(), 
RotatedPole::inverseParameter);
+        sinθp   =  forward.sinθp;
+        cosθp   = -forward.cosθp;
+        inverse =  forward;
+    }
+
+    /**
+     * Computes the value of the given parameter for the inverse operation.
+     * This method is invoked for each parameter.
+     *
+     * @param  forward  the forward operation.
+     * @param  target   parameter to initialize.
+     * @return whether to accept the parameter (always {@code true}).
+     */
+    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 = Math.IEEEremainder(value + 180, 360);
+                target.setValue(value);
+                return true;
+            }
+        }
+        return false;       // Should never happen.
+    }
+
+    /**
+     * Creates the non-linear part of a rotated pole operation.
+     * This transform does not include the conversion between degrees and 
radians and the longitude rotations.
+     * For a complete transform, use one of the static factory methods.
+     *
+     * @param  south  {@code true} for a south pole rotation, or {@code false} 
for a north pole rotation.
+     * @param  λp     geographic longitude in degrees of the southern pole of 
the coordinate system.
+     * @param  θp     geographic latitude 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
+     *                looking from the southern to the northern pole.
+     */
+    protected RotatedPole(final boolean south, double λp, double θp, double 
pa) {
+        context = new ContextualParameters(
+                south ? RotatedSouthPole.PARAMETERS
+                      : RotatedNorthPole.PARAMETERS, 2, 2);
+        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
+        final double θ = toRadians(θp);
+        final double sign = south ? 1 : -1;
+        sinθp = sin(θ) * sign;
+        cosθp = cos(θ) * sign;
+        context.normalizeGeographicInputs(λp);
+        context.denormalizeGeographicOutputs(-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);
+    }
+
+    /**
+     * Creates a new rotated south pole operation.
+     *
+     * @param  factory  the factory to use for creating the transform.
+     * @param  λp       geographic longitude in degrees of the southern pole 
of the coordinate system.
+     * @param  θp       geographic latitude 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
+     *                  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 RotatedPole kernel = new RotatedPole(true, λp, θp, pa);
+        return kernel.context.completeTransform(factory, kernel);
+    }
+
+    /**
+     * Creates a new rotated north pole operation.
+     *
+     * @param  factory  the factory to use for creating the transform.
+     * @param  λp       geographic longitude in degrees of the northern pole 
of the coordinate system.
+     * @param  θp       geographic latitude 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
+     *                  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.
+     */
+    public static MathTransform rotateNorthPole(final MathTransformFactory 
factory,
+            final double λp, final double θp, final double pa) throws 
FactoryException
+    {
+        final RotatedPole kernel = new RotatedPole(false, λp, θp, pa);
+        return kernel.context.completeTransform(factory, kernel);
+    }
+
+    /**
+     * Returns a description of the parameters of this transform. The group of 
parameters contains only the grid
+     * (north or south) pole latitude. It does not contain the grid pole 
longitude or the grid angle of rotation
+     * because those parameters are handled by affine transforms pre- or 
post-concatenated to this transform.
+     *
+     * @return the parameter descriptors for this math transform.
+     */
+    @Debug
+    @Override
+    public ParameterDescriptorGroup getParameterDescriptors() {
+        // We assume that it is not worth to cache this descriptor.
+        ImmutableIdentifier name = new ImmutableIdentifier(Citations.SIS, 
Constants.SIS, "Rotated Latitude/longitude (radians domain)");
+        return new 
DefaultParameterDescriptorGroup(Collections.singletonMap(ParameterDescriptorGroup.NAME_KEY,
 name),
+                1, 1, (ParameterDescriptor<?>) 
context.getDescriptor().descriptors().get(0));
+    }
+
+    /**
+     * Returns a copy of the parameter values of this transform.
+     * The group contains the values of the parameters described by {@link 
#getParameterDescriptors()}.
+     * This method is mostly for {@linkplain 
org.apache.sis.io.wkt.Convention#INTERNAL debugging purposes};
+     * most GIS applications will instead be interested in the {@linkplain 
#getContextualParameters()
+     * contextual parameters} instead.
+     *
+     * @return the parameter values for this math transform.
+     */
+    @Debug
+    @Override
+    public ParameterValueGroup getParameterValues() {
+        final ParameterValueGroup values = 
getParameterDescriptors().createValue();
+        values.values().add(context.values().get(0));           // First 
parameter is grid pole latitude.
+        return values;
+    }
+
+    /**
+     * Returns the parameters used for creating the complete operation. The 
returned group contains not only
+     * the grid pole latitude (which is handled by this transform), but also 
the grid pole longitude and the
+     * grid angle of rotation (which are handled by affine transforms before 
or after this transform).
+     *
+     * @return the parameter values for the sequence of <cite>normalize</cite> 
→
+     *         {@code this} → <cite>denormalize</cite> transforms.
+     */
+    @Override
+    protected ContextualParameters getContextualParameters() {
+        return context;
+    }
+
+    /**
+     * Transforms a single coordinate point in an array,
+     * and optionally computes the transform derivative at that location.
+     */
+    @Override
+    public Matrix transform(final double[] srcPts, final int srcOff,
+                            final double[] dstPts, final int dstOff,
+                            final boolean derivate) throws TransformException
+    {
+        /*
+         * Convert latitude and longitude coordinates to (x,y,z) Cartesian 
coordinates on a sphere of radius 1.
+         * Note that the rotation around the Z axis has been performed in 
geographic coordinates by the affine
+         * transform pre-concatenated to this transform, simply by subtracting 
λp from the longitude value.
+         * This is simpler than performing the rotation in Cartesian 
coordinates.
+         */
+        double λ    = srcPts[srcOff];
+        double φ    = srcPts[srcOff+1];
+        double z    = sin(φ);
+        double cosφ = cos(φ);
+        double y    = sin(λ) * cosφ;
+        double x    = cos(λ) * cosφ;
+        /*
+         * Apply the rotation around Y axis (so the y value stay unchanged)
+         * and convert back to spherical coordinates.
+         */
+        double xr =  cosθp * z - sinθp * x;
+        double zr = -cosθp * x - sinθp * z;
+        double R  = sqrt(xr*xr + y*y);          // The slower hypot(…) is not 
needed because values are close to 1.
+        dstPts[dstOff]   = atan2(y, xr);
+        dstPts[dstOff+1] = atan2(zr, R);
+        if (!derivate) {
+            return null;
+        }
+        throw new TransformException();         // TODO
+    }
+
+    /**
+     * Returns the inverse transform of this object.
+     *
+     * @return the inverse of this transform.
+     */
+    @Override
+    public synchronized MathTransform2D inverse() {
+        if (inverse == null) {
+            inverse = new RotatedPole(this);
+        }
+        return inverse;
+    }
+
+    /**
+     * Compares the specified object with this math transform for equality.
+     *
+     * @param  object  the object to compare with this transform.
+     * @param  mode    the strictness level of the comparison.
+     * @return {@code true} if the given object is considered equals to this 
math transform.
+     */
+    @Override
+    public boolean equals(final Object object, final ComparisonMode mode) {
+        if (super.equals(object, mode)) {
+            final RotatedPole other = (RotatedPole) object;
+            if (mode.isApproximate()) {
+                return Numerics.epsilonEqual(sinθp, other.sinθp, 
Formulas.ANGULAR_TOLERANCE * (PI/180)) &&
+                       Numerics.epsilonEqual(cosθp, other.cosθp, 
Formulas.ANGULAR_TOLERANCE * (PI/180));
+            } else {
+                return Numerics.equals(sinθp, other.sinθp) &&
+                       Numerics.equals(cosθp, other.cosθp);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Computes a hash value for this transform. This method is invoked by 
{@link #hashCode()} when first needed.
+     */
+    @Override
+    protected int computeHashCode() {
+        return super.computeHashCode() + Double.hashCode(cosθp) + 
Double.hashCode(sinθp);
+    }
+}
diff --git 
a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
 
b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
index 429a260..2526f0e 100644
--- 
a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
+++ 
b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
@@ -63,6 +63,8 @@ 
org.apache.sis.internal.referencing.provider.ZonedTransverseMercator
 org.apache.sis.internal.referencing.provider.Sinusoidal
 org.apache.sis.internal.referencing.provider.Polyconic
 org.apache.sis.internal.referencing.provider.Mollweide
+org.apache.sis.internal.referencing.provider.RotatedSouthPole
+org.apache.sis.internal.referencing.provider.RotatedNorthPole
 org.apache.sis.internal.referencing.provider.NTv2
 org.apache.sis.internal.referencing.provider.NTv1
 org.apache.sis.internal.referencing.provider.NADCON
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
index e3b4513..21351a0 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
@@ -36,7 +36,7 @@ import static org.junit.Assert.*;
  * Tests {@link Providers} and some consistency rules of all providers defined 
in this package.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.6
  * @module
  */
@@ -113,6 +113,8 @@ public final strictfp class ProvidersTest extends TestCase {
             Sinusoidal.class,
             Polyconic.class,
             Mollweide.class,
+            RotatedSouthPole.class,
+            RotatedNorthPole.class,
             NTv2.class,
             NTv1.class,
             NADCON.class,
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/RotatedPoleTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/RotatedPoleTest.java
new file mode 100644
index 0000000..8cca70c
--- /dev/null
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/RotatedPoleTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.transform;
+
+import org.junit.Test;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.parameter.Parameterized;
+import org.apache.sis.test.DependsOnMethod;
+
+
+/**
+ * Tests {@link RotatedPole}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public final strictfp class RotatedPoleTest extends MathTransformTestCase {
+    /**
+     * Returns the transform factory to use for testing purpose.
+     * This mock supports only the "affine" and "concatenate" operations.
+     */
+    private static MathTransformFactory factory() {
+        return new MathTransformFactoryMock(null);
+    }
+
+    /**
+     * Creates a new transform which should be the inverse of current 
transform according the
+     * parameters declared in {@link RotatedPole#context}. Those parameters 
may be wrong even
+     * if the coordinates transformed by {@code transform.inverse()} are 
corrects because the
+     * parameters are only for WKT formatting (they are not actually used for 
transformation,
+     * unless we force their use as done in this method).
+     */
+    private void inverseSouthPoleTransform() throws FactoryException, 
TransformException {
+        final ParameterValueGroup pg = ((Parameterized) 
transform.inverse()).getParameterValues();
+        transform = RotatedPole.rotateSouthPole(factory(),
+                pg.parameter("grid_south_pole_longitude").doubleValue(),
+                pg.parameter("grid_south_pole_latitude") .doubleValue(),
+                pg.parameter("grid_south_pole_angle")    .doubleValue());
+
+    }
+
+    /**
+     * Tests a rotation of south pole with the new pole on Greenwich.
+     * The {@link ucar.unidata.geoloc.LatLonPoint} class has been used
+     * as a reference implementation for computing the expected values.
+     *
+     * @throws FactoryException if the transform can not be created.
+     * @throws TransformException if an error occurred while transforming a 
point.
+     */
+    @Test
+    public void testRotateSouthPoleOnGreenwich() throws FactoryException, 
TransformException {
+        transform = RotatedPole.rotateSouthPole(factory(), 0, -60, 0);
+        tolerance = Formulas.ANGULAR_TOLERANCE;
+        isDerivativeSupported = false;
+        final double[] coordinates = {      // (λ,φ) coordinates to convert.
+              0, -51,
+             20, -51,
+            100, -61
+        };
+        final double[] expected = {         // (λ,φ) coordinates after 
conversion.
+              0.000000000, -81.000000000,
+             60.140453893, -75.629715301,
+            136.900518716, -45.671868261
+        };
+        verifyTransform(coordinates, expected);
+        inverseSouthPoleTransform();
+        verifyTransform(expected, coordinates);
+    }
+
+    /**
+     * Tests a rotation of south pole with the pole on arbitrary meridian.
+     * The {@link ucar.unidata.geoloc.LatLonPoint} class has been used as
+     * a reference implementation for computing the 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 testRotateSouthPoleWithAngle() throws FactoryException, 
TransformException {
+        transform = RotatedPole.rotateSouthPole(factory(), 20, -50, 10);
+        tolerance = Formulas.ANGULAR_TOLERANCE;
+        isDerivativeSupported = false;
+        final double[] coordinates = {      // (λ,φ) coordinates to convert.
+             20, -51,
+             80, -44,
+            -30, -89
+        };
+        final double[] expected = {         // (λ,φ) coordinates after 
conversion.
+             170.000000000, -89.000000000,
+              95.348788748, -49.758697265,
+            -188.792151374, -50.636582758
+        };
+        verifyTransform(coordinates, expected);
+        inverseSouthPoleTransform();
+        verifyTransform(expected, coordinates);
+    }
+}
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index 42130b8..412d597 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -142,6 +142,7 @@ import org.junit.BeforeClass;
     
org.apache.sis.referencing.operation.transform.EllipsoidToCentricTransformTest.class,
     
org.apache.sis.referencing.operation.transform.MolodenskyTransformTest.class,
     
org.apache.sis.referencing.operation.transform.AbridgedMolodenskyTransformTest.class,
+    org.apache.sis.referencing.operation.transform.RotatedPoleTest.class,
     
org.apache.sis.referencing.operation.transform.SphericalToCartesianTest.class,
     
org.apache.sis.referencing.operation.transform.CartesianToSphericalTest.class,
     org.apache.sis.referencing.operation.transform.PolarToCartesianTest.class,

Reply via email to