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,