| /* |
| * 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.provider; |
| |
| import java.util.Map; |
| import jakarta.xml.bind.annotation.XmlTransient; |
| import javax.measure.Unit; |
| import org.opengis.util.FactoryException; |
| import org.opengis.parameter.ParameterDescriptor; |
| import org.opengis.parameter.ParameterDescriptorGroup; |
| import org.opengis.parameter.ParameterNotFoundException; |
| import org.opengis.parameter.InvalidParameterValueException; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.apache.sis.metadata.iso.citation.Citations; |
| import org.apache.sis.parameter.ParameterBuilder; |
| import org.apache.sis.parameter.Parameters; |
| import org.apache.sis.referencing.datum.DefaultEllipsoid; |
| import org.apache.sis.referencing.operation.transform.MolodenskyTransform; |
| import org.apache.sis.referencing.privy.NilReferencingObject; |
| import org.apache.sis.referencing.privy.Formulas; |
| import org.apache.sis.util.Debug; |
| import org.apache.sis.util.privy.Constants; |
| import org.apache.sis.measure.Units; |
| |
| |
| /** |
| * The provider for <q>Molodensky transformation</q> (EPSG:9604). |
| * This provider constructs transforms between two geographic reference systems without passing though a geocentric one. |
| * This class nevertheless extends {@link GeocentricAffineBetweenGeographic} because it is an approximation of |
| * {@link GeocentricTranslation3D}. |
| * |
| * <p>The translation terms (<var>dx</var>, <var>dy</var> and <var>dz</var>) are common to all authorities. |
| * But remaining parameters are specified in different ways depending on the authority:</p> |
| * |
| * <ul> |
| * <li>EPSG defines <q>Semi-major axis length difference</q> |
| * and <q>Flattening difference</q> parameters.</li> |
| * <li>OGC rather defines "{@code src_semi_major}", "{@code src_semi_minor}", |
| * "{@code tgt_semi_major}", "{@code tgt_semi_minor}" and "{@code dim}" parameters.</li> |
| * </ul> |
| * |
| * @author Rueben Schulz (UBC) |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| */ |
| @XmlTransient |
| public final class Molodensky extends GeocentricAffineBetweenGeographic { |
| /** |
| * Serial number for inter-operability with different versions. |
| */ |
| private static final long serialVersionUID = 8126525068450868912L; |
| |
| /** |
| * The operation parameter descriptor for the <cite>Semi-major axis length difference</cite> |
| * optional parameter value. This parameter is defined by the EPSG database and can be used |
| * in replacement of {@link #TGT_SEMI_MAJOR}. |
| * Units are {@linkplain Units#METRE metres}. |
| * |
| * <!-- Generated by ParameterNameTableGenerator --> |
| * <table class="sis"> |
| * <caption>Parameter names</caption> |
| * <tr><td> EPSG: </td><td> Semi-major axis length difference </td></tr> |
| * </table> |
| * <b>Notes:</b> |
| * <ul> |
| * <li>No default value</li> |
| * </ul> |
| */ |
| public static final ParameterDescriptor<Double> AXIS_LENGTH_DIFFERENCE; |
| |
| /** |
| * The operation parameter descriptor for the <cite>Flattening difference</cite> optional parameter value. |
| * This parameter is defined by the EPSG database and can be used in replacement of {@link #TGT_SEMI_MINOR}. |
| * Valid values range from -1 to +1, {@linkplain Units#UNITY dimensionless}. |
| * |
| * <!-- Generated by ParameterNameTableGenerator --> |
| * <table class="sis"> |
| * <caption>Parameter names</caption> |
| * <tr><td> EPSG: </td><td> Flattening difference </td></tr> |
| * </table> |
| * <b>Notes:</b> |
| * <ul> |
| * <li>Value domain: [-1.0 … 1.0]</li> |
| * <li>No default value</li> |
| * </ul> |
| */ |
| public static final ParameterDescriptor<Double> FLATTENING_DIFFERENCE; |
| |
| /** |
| * The group of all parameters expected by this coordinate operation. |
| */ |
| public static final ParameterDescriptorGroup PARAMETERS; |
| static { |
| final ParameterBuilder builder = builder(); |
| AXIS_LENGTH_DIFFERENCE = builder.addIdentifier("8654").addName("Semi-major axis length difference").create(Double.NaN, Units.METRE); |
| FLATTENING_DIFFERENCE = builder.addIdentifier("8655").addName("Flattening difference").createBounded(-1, +1, Double.NaN, Units.UNITY); |
| PARAMETERS = builder.setRequired(true) |
| .addIdentifier("9604") |
| .addName("Molodensky") |
| .addName(Citations.OGC, "Molodenski") |
| .createGroup(DIMENSION, // OGC only |
| SRC_SEMI_MAJOR, SRC_SEMI_MINOR, // OGC only |
| TGT_SEMI_MAJOR, TGT_SEMI_MINOR, // OGC only - redundant with differences |
| TX, // OGC and EPSG |
| TY, // OGC and EPSG |
| TZ, // OGC and EPSG |
| AXIS_LENGTH_DIFFERENCE, // EPSG only |
| FLATTENING_DIFFERENCE); // EPSG only |
| } |
| |
| /** |
| * Creates a descriptor for the internal parameters of {@link MolodenskyTransform}. |
| * This is similar to the standard parameters except that the redundant target axes |
| * lengths are omitted. |
| * |
| * @return internal parameter descriptor. |
| */ |
| @Debug |
| public static ParameterDescriptorGroup internal() { |
| final ParameterBuilder builder = builder().setCodeSpace(Citations.SIS, Constants.SIS); |
| ParameterDescriptor<Boolean> abridged = builder.addName("abridged").create(Boolean.class, null); |
| return builder.addName("Molodensky (radians domain)") |
| .createGroup(DIMENSION, |
| SRC_SEMI_MAJOR, |
| SRC_SEMI_MINOR, |
| AXIS_LENGTH_DIFFERENCE, |
| FLATTENING_DIFFERENCE, |
| TX, |
| TY, |
| TZ, |
| abridged); |
| } |
| |
| /** |
| * Creates a new provider. |
| */ |
| public Molodensky() { |
| super(Type.MOLODENSKY, PARAMETERS, (byte) 2); |
| } |
| |
| /** |
| * Creates a Molodensky transform from the specified group of parameter values. |
| * |
| * @param context the parameter values together with its context. |
| * @return the created Molodensky transform. |
| * @throws FactoryException if a transform cannot be created. |
| */ |
| @Override |
| public MathTransform createMathTransform(Context context) throws FactoryException { |
| return createMathTransform(context, false); |
| } |
| |
| /** |
| * Creates a (potentially abridged) Molodensky transform from the specified group of parameter values. |
| * If a specified number of dimensions is absent, it will be overridden by user parameters. |
| * |
| * @param context the parameter values together with its context. |
| * @param isAbridged {@code true} for the abridged form. |
| * @return the created (abridged) Molodensky transform. |
| * @throws FactoryException if a transform cannot be created. |
| */ |
| static MathTransform createMathTransform(final Context context, final boolean isAbridged) throws FactoryException { |
| final Parameters values = Parameters.castOrWrap(context.getCompletedParameters()); |
| final int dim = values.getValue(DIMENSION); |
| /* |
| * Following method calls implicitly convert parameter values to metres. |
| * We do not try to match ellipsoid axis units because: |
| * |
| * 1) It complicates the code. |
| * 2) We have no guarantees that ellipsoid unit match the coordinate system unit. |
| * 3) OGC 01-009 explicitly said that angles are in degrees and heights in metres. |
| * 4) The above is consistent with what we do for map projections. |
| */ |
| double sa = values.doubleValue( SRC_SEMI_MAJOR); |
| double sb = values.doubleValue( SRC_SEMI_MINOR); |
| double ta = optional (values, TGT_SEMI_MAJOR); |
| double tb = optional (values, TGT_SEMI_MINOR); |
| double Δa = conditional(values, AXIS_LENGTH_DIFFERENCE, ta); |
| double Δf = conditional(values, FLATTENING_DIFFERENCE, tb); |
| if (Double.isNaN(ta)) { |
| ta = sa + Δa; |
| } |
| if (Double.isNaN(tb)) { |
| tb = ta*(sb/sa - Δf); |
| } |
| final Map<String,?> name = Map.of(DefaultEllipsoid.NAME_KEY, NilReferencingObject.UNNAMED); |
| final Ellipsoid source = new Ellipsoid(name, sa, sb, Δa, Δf); |
| final Ellipsoid target = new Ellipsoid(name, ta, tb, -Δa, -Δf); |
| source.other = target; |
| target.other = source; |
| source.computeDifferences(values); |
| return MolodenskyTransform.createGeodeticTransformation( |
| context.getFactory(), |
| source, context.getSourceDimensions().orElse(dim) >= 3, |
| target, context.getTargetDimensions().orElse(dim) >= 3, |
| values.doubleValue(TX), |
| values.doubleValue(TY), |
| values.doubleValue(TZ), |
| isAbridged); |
| } |
| |
| /** |
| * Returns the value of the given parameter, or NaN if undefined. |
| */ |
| private static double optional(final Parameters values, final ParameterDescriptor<Double> parameter) { |
| final Double value = values.getValue(parameter); |
| return (value != null) ? value : Double.NaN; |
| } |
| |
| /** |
| * Returns the value of the given parameter as a mandatory parameter if {@code condition} is NaN, |
| * or an optional parameter otherwise. |
| */ |
| private static double conditional(Parameters values, ParameterDescriptor<Double> parameter, double condition) { |
| return Double.isNaN(condition) ? values.doubleValue(parameter) : optional(values, parameter); |
| } |
| |
| /** |
| * A temporary ellipsoid used only for passing arguments to the {@link MolodenskyTransform} constructor. |
| * The intent is to use the Δa and Δf values explicitly specified in the EPSG parameters if available, |
| * or to compute them only if no Δa or Δf values where specified. |
| */ |
| @SuppressWarnings("serial") |
| private static final class Ellipsoid extends DefaultEllipsoid { |
| /** The EPSG parameter values, or NaN if unspecified. */ |
| private double Δa, Δf; |
| |
| /** The ellipsoid for which Δa and Δf are valid. */ |
| Ellipsoid other; |
| |
| /** Creates a new temporary ellipsoid with explicitly provided Δa and Δf values. */ |
| Ellipsoid(Map<String,?> name, double a, double b, double Δa, double Δf) { |
| super(name, a, b, Formulas.getInverseFlattening(a, b), false, Units.METRE); |
| this.Δa = Δa; |
| this.Δf = Δf; |
| } |
| |
| /** |
| * Computes Δa and Δf now if not already done and tries to store the result in the given parameters. |
| * The parameters are set in order to complete them when the user specified the OGC parameters (axis |
| * lengths) and not the EPSG ones (axis and flattening differences). |
| */ |
| void computeDifferences(final Parameters values) { |
| if (Double.isNaN(Δa)) { |
| Δa = super.semiMajorAxisDifference(other); |
| setIfPresent(values, AXIS_LENGTH_DIFFERENCE, Δa, getAxisUnit()); |
| } |
| if (Double.isNaN(Δf)) { |
| Δf = super.flatteningDifference(other); |
| setIfPresent(values, FLATTENING_DIFFERENCE, Δf, Units.UNITY); |
| } |
| } |
| |
| /** |
| * Tries to set the given parameter value. This method should be invoked only when completing parameters |
| * without explicit values. This approach completes the work done in {@code DefaultMathTransformFactory}, |
| * which already completed the {@code src_semi_major}, {@code src_semi_minor}, {@code tgt_semi_major} and |
| * {@code tgt_semi_minor} parameters. |
| * |
| * @param values the group in which to set the parameter values. |
| * @param parameter descriptor of the parameter to set. |
| * @param value the new value. |
| * @param unit unit of measurement for the new value. |
| */ |
| private static void setIfPresent(final Parameters values, final ParameterDescriptor<Double> parameter, |
| final double value, final Unit<?> unit) |
| { |
| try { |
| values.getOrCreate(parameter).setValue(value, unit); |
| } catch (ParameterNotFoundException | InvalidParameterValueException e) { |
| // Nonn-fatal since this attempt was only for information purpose. |
| recoverableException(Molodensky.class, e); |
| } |
| } |
| |
| /** Returns Δa as specified in the parameters if possible, or compute it otherwise. */ |
| @Override public double semiMajorAxisDifference(final org.opengis.referencing.datum.Ellipsoid target) { |
| return (target == other && !Double.isNaN(Δa)) ? Δa : super.semiMajorAxisDifference(target); |
| } |
| |
| /** Returns Δf as specified in the parameters if possible, or compute it otherwise. */ |
| @Override public double flatteningDifference(final org.opengis.referencing.datum.Ellipsoid target) { |
| return (target == other && !Double.isNaN(Δf)) ? Δf : super.flatteningDifference(target); |
| } |
| } |
| } |