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 4956bf7511f6840524685a3c021b96b871b1878b Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Oct 28 23:29:32 2023 +0200 Following the removal of `InterpolatedMolodenskyTransform`, retrofit `MolodenskyFormula` into `MolodenskyTransform`. --- .../main/module-info.java | 2 +- .../transform/AbridgedMolodenskyTransform2D.java | 2 +- .../operation/transform/MolodenskyFormula.java | 463 --------------------- .../operation/transform/MolodenskyTransform.java | 418 ++++++++++++++++++- 4 files changed, 401 insertions(+), 484 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/module-info.java b/endorsed/src/org.apache.sis.referencing/main/module-info.java index 6b668db7a6..af2202a759 100644 --- a/endorsed/src/org.apache.sis.referencing/main/module-info.java +++ b/endorsed/src/org.apache.sis.referencing/main/module-info.java @@ -21,7 +21,7 @@ * @author Martin Desruisseaux (Geomatys) * @author Rémi Maréchal (Geomatys) * @author Maxime Gavens (Geomatys) - * @version 1.4 + * @version 1.5 * @since 0.3 */ module org.apache.sis.referencing { diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransform2D.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransform2D.java index d1ca0e7fd7..0eaef9b946 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransform2D.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbridgedMolodenskyTransform2D.java @@ -26,7 +26,7 @@ import static java.lang.Math.*; /** * Two-dimensional abridged Molodensky transform with all translation terms fixed to zero. * This implementation performs only a change of ellipsoid. It provides nothing new compared - * to {@link MolodenskyFormula}, except performance. + * to {@link MolodenskyTransform}, except performance. * * <p><b>Note:</b> this transform is yet more abridged than standard "abridged Molondensky" transform since * it sets all translation terms to zero. A better class name could be "Very abridged Molodensky transform". diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java deleted file mode 100644 index 63c0a9f3e9..0000000000 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java +++ /dev/null @@ -1,463 +0,0 @@ -/* - * 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 javax.measure.Unit; -import javax.measure.quantity.Length; -import org.opengis.parameter.ParameterValueGroup; -import org.opengis.parameter.ParameterDescriptorGroup; -import org.opengis.referencing.datum.Ellipsoid; -import org.opengis.referencing.operation.Matrix; -import org.opengis.referencing.operation.TransformException; -import org.apache.sis.referencing.operation.matrix.Matrices; -import org.apache.sis.referencing.datum.DefaultEllipsoid; -import org.apache.sis.referencing.datum.DatumShiftGrid; -import org.apache.sis.referencing.operation.provider.Molodensky; -import org.apache.sis.util.ArgumentChecks; -import org.apache.sis.util.ComparisonMode; -import org.apache.sis.util.Debug; -import org.apache.sis.util.internal.Numerics; -import org.apache.sis.parameter.Parameters; - -import static java.lang.Math.*; - - -/** - * Implementation of Molodensky formulas. - * - * @author Martin Desruisseaux (Geomatys) - */ -abstract class MolodenskyFormula extends DatumShiftTransform { - /** - * Serial number for inter-operability with different versions. - */ - private static final long serialVersionUID = 7684676923384073055L; - - /** - * The value of 1/sin(1″) multiplied by the conversion factor from arc-seconds to radians (π/180)/(60⋅60). - * This is the final multiplication factor for Δλ and Δφ. - */ - static final double ANGULAR_SCALE = 1.00000000000391744; - - /** - * {@code true} if the source coordinates have a height. - */ - final boolean isSource3D; - - /** - * {@code true} if the target coordinates have a height. - */ - final boolean isTarget3D; - - /** - * {@code true} for the abridged formula, or {@code false} for the complete one. - */ - final boolean isAbridged; - - /** - * Shift along the geocentric X axis (toward prime meridian) - * in units of the semi-major axis of the source ellipsoid. - * - * @see org.apache.sis.referencing.datum.BursaWolfParameters#tX - */ - protected final double tX; - - /** - * Shift along the geocentric Y axis (toward 90°E) - * in units of the semi-major axis of the source ellipsoid. - * - * @see org.apache.sis.referencing.datum.BursaWolfParameters#tY - */ - protected final double tY; - - /** - * Shift along the geocentric Z axis (toward north pole) - * in units of the semi-major axis of the source ellipsoid. - * - * @see org.apache.sis.referencing.datum.BursaWolfParameters#tZ - */ - protected final double tZ; - - /** - * Difference in the semi-major axes of the target and source ellipsoids: {@code Δa = target a - source a}. - * - * @see DefaultEllipsoid#semiMajorAxisDifference(Ellipsoid) - */ - final double Δa; - - /** - * Difference between the flattening of the target and source ellipsoids (Δf), opportunistically modified - * with additional terms. The value depends on whether this Molodensky transform is abridged or not: - * - * <ul> - * <li>For Molodensky, this field is set to (b⋅Δf).</li> - * <li>For Abridged Molodensky, this field is set to (a⋅Δf) + (f⋅Δa).</li> - * </ul> - * - * where Δf = <var>target flattening</var> - <var>source flattening</var>. - */ - final double Δfmod; - - /** - * Semi-major axis length (<var>a</var>) of the source ellipsoid. - */ - protected final double semiMajor; - - /** - * The square of eccentricity of the source ellipsoid. - * This can be computed by ℯ² = (a²-b²)/a² where - * <var>a</var> is the <cite>semi-major</cite> axis length and - * <var>b</var> is the <cite>semi-minor</cite> axis length. - * - * @see DefaultEllipsoid#getEccentricitySquared() - */ - protected final double eccentricitySquared; - - /** - * Constructs the inverse of a Molodensky transform. - * - * @param inverse the transform for which to create the inverse. - * @param source the source ellipsoid of the given {@code inverse} transform. - * @param target the target ellipsoid of the given {@code inverse} transform. - * @param descriptor the contextual parameter descriptor. - */ - MolodenskyFormula(final MolodenskyFormula inverse, final Ellipsoid source, final Ellipsoid target, - final ParameterDescriptorGroup descriptor) - { - this(target, inverse.isTarget3D, - source, inverse.isSource3D, - -inverse.tX, -inverse.tY, -inverse.tZ, inverse.grid, inverse.isAbridged, descriptor); - } - - /** - * Creates a Molodensky transform from the specified parameters. - * If a non-null {@code grid} is specified, it is caller's responsibility to verify its validity. - * - * @param source the source ellipsoid. - * @param isSource3D {@code true} if the source coordinates have a height. - * @param target the target ellipsoid. - * @param isTarget3D {@code true} if the target coordinates have a height. - * @param tX the geocentric <var>X</var> translation in same units than the source ellipsoid axes. - * @param tY the geocentric <var>Y</var> translation in same units than the source ellipsoid axes. - * @param tZ the geocentric <var>Z</var> translation in same units than the source ellipsoid axes. - * @param grid interpolation grid in geocentric coordinates, or {@code null} if none. - * @param isAbridged {@code true} for the abridged formula, or {@code false} for the complete one. - * @param descriptor the contextual parameter descriptor. - */ - @SuppressWarnings("OverridableMethodCallInConstructor") - MolodenskyFormula(final Ellipsoid source, final boolean isSource3D, - final Ellipsoid target, final boolean isTarget3D, - final double tX, final double tY, final double tZ, - final DatumShiftGrid<?,?> grid, final boolean isAbridged, - final ParameterDescriptorGroup descriptor) - { - super(descriptor, isSource3D, isTarget3D, grid); - ArgumentChecks.ensureNonNull("source", source); - ArgumentChecks.ensureNonNull("target", target); - final DefaultEllipsoid src = DefaultEllipsoid.castOrCopy(source); - this.isSource3D = isSource3D; - this.isTarget3D = isTarget3D; - this.isAbridged = isAbridged; - this.semiMajor = src.getSemiMajorAxis(); - this.Δa = src.semiMajorAxisDifference(target); - this.tX = tX; - this.tY = tY; - this.tZ = tZ; - - final double semiMinor = src.getSemiMinorAxis(); - final double Δf = src.flatteningDifference(target); - eccentricitySquared = src.getEccentricitySquared(); - Δfmod = isAbridged ? (semiMajor * Δf) + (semiMajor - semiMinor) * (Δa / semiMajor) - : (semiMinor * Δf); - /* - * Copy parameters to the ContextualParameter. Those parameters are not used directly - * by MolodenskyTransform, but we need to store them in case the user asks for them. - */ - final Unit<Length> unit = src.getAxisUnit(); - setContextParameters(semiMajor, semiMinor, unit, target); - completeParameters(context, semiMinor, unit, Δf); - /* - * Prepare two affine transforms to be executed before and after the MolodenskyTransform: - * - * - A "normalization" transform for converting degrees to radians, - * - A "denormalization" transform for for converting radians to degrees. - */ - context.normalizeGeographicInputs(0); - context.denormalizeGeographicOutputs(0); - } - - /** - * Returns a copy of internal parameter values of this transform. - * The returned group contains parameters for the source ellipsoid semi-axis lengths - * and the differences between source and target ellipsoid parameters. - * - * <h4>Usage note</h4> - * This method is mostly for {@linkplain org.apache.sis.io.wkt.Convention#INTERNAL debugging purposes} - * since the isolation of non-linear parameters in this class is highly implementation dependent. - * Most GIS applications will instead be interested in the {@linkplain #getContextualParameters() - * contextual parameters}. - * - * @return a copy of the internal parameter values for this transform. - */ - @Debug - @Override - public ParameterValueGroup getParameterValues() { - final Unit<?> unit = context.getOrCreate(Molodensky.SRC_SEMI_MAJOR).getUnit(); - final double semiMinor = context.getOrCreate(Molodensky.SRC_SEMI_MINOR).doubleValue(unit); - final Parameters pg = Parameters.castOrWrap(getParameterDescriptors().createValue()); - pg.getOrCreate(Molodensky.SRC_SEMI_MAJOR).setValue(semiMajor, unit); - pg.getOrCreate(Molodensky.SRC_SEMI_MINOR).setValue(semiMinor, unit); - completeParameters(pg, semiMinor, unit, Double.NaN); - return pg; - } - - /** - * Sets parameter values in the given group for parameters other than axis lengths. - * This method is invoked for both completing contextual parameters ({@code pg == context}) and - * for completing internal parameters ({@code pg != context}). When this method is invoked, the - * following parameters are already set: - * - * <ul> - * <li>{@code "src_semi_major"}</li> - * <li>{@code "src_semi_minor"}</li> - * <li>{@code "tgt_semi_major"} (contextual parameters only)</li> - * <li>{@code "tgt_semi_minor"} (contextual parameters only)</li> - * </ul> - * - * The default implementation sets the {@code "dim"} parameter. - * Subclasses shall override this method for completing also the following parameters: - * - * <ul> - * <li><cite>"X-axis translation"</cite> (Molodensky only)</li> - * <li><cite>"Y-axis translation"</cite> (Molodensky only)</li> - * <li><cite>"Z-axis translation"</cite> (Molodensky only)</li> - * <li><cite>"Geocentric translation file"</cite> (Geocentric interpolations only)</li> - * <li><cite>"Semi-major axis length difference"</cite> (Always for Molodensky, internal WKT only for geocentric interpolations)</li> - * <li><cite>"Flattening difference"</cite> (Always for Molodensky, internal WKT only for geocentric interpolations)</li> - * </ul> - * - * @param pg where to set the parameters. - * @param semiMinor the semi minor axis length, in unit of {@code unit}. - * @param unit the unit of measurement to declare. - * @param Δf the flattening difference to set, or NaN if this method should fetch that value itself. - */ - void completeParameters(final Parameters pg, final double semiMinor, final Unit<?> unit, final double Δf) { - /* - * Unconditionally set the "dim" parameters to the number of source dimensions (do not check for consistency - * with the number of target dimensions) because source dimensions determine the value of ellipsoidal heights, - * which may change the horizontal numerical values. By contrast, the number of target dimensions does not have - * any impact on numerical values (it can just causes a drop of the third value). - */ - pg.getOrCreate(Molodensky.DIMENSION).setValue(getSourceDimensions()); - } - - /** - * Gets the dimension of input points. - * - * @return the input dimension, which is 2 or 3. - */ - @Override - public final int getSourceDimensions() { - return isSource3D ? 3 : 2; - } - - /** - * Gets the dimension of output points. - * - * @return the output dimension, which is 2 or 3. - */ - @Override - public final int getTargetDimensions() { - return isTarget3D ? 3 : 2; - } - - /** - * Implementation of {@link #transform(double[], int, double[], int, boolean)} with possibility - * to override some field values. In this method signature, parameters having the same name than - * fields have the same value. - * - * @param λ longitude (radians). - * @param φ latitude (radians). - * @param h height above the ellipsoid in unit of semi-major axis. - * @param dstPts the array into which the transformed coordinate is returned, or {@code null}. - * @param dstOff the offset to the location of the transformed point that is stored in the destination array. - * @param tX the {@link #tX} field value (or a slightly different value during geocentric interpolation). - * @param tY the {@link #tY} field value (or a slightly different value during geocentric interpolation). - * @param tZ the {@link #tZ} field value (or a slightly different value during geocentric interpolation). - * @param derivate {@code true} for computing the derivative, or {@code false} if not needed. - * @throws TransformException if a point cannot be transformed. - */ - final Matrix transform(final double λ, final double φ, final double h, final double[] dstPts, int dstOff, - double tX, double tY, double tZ, final boolean derivate) - throws TransformException - { - /* - * Abridged Molodensky formulas from EPSG guidance note: - * - * ν = a / √(1 - ℯ²⋅sin²φ) : radius of curvature in the prime vertical - * ρ = a⋅(1 – ℯ²) / (1 – ℯ²⋅sin²φ)^(3/2) : radius of curvature in the meridian - * Δλ″ = (-tX⋅sinλ + tY⋅cosλ) / (ν⋅cosφ⋅sin1″) - * Δφ″ = (-tX⋅sinφ⋅cosλ - tY⋅sinφ⋅sinλ + tZ⋅cosφ + [a⋅Δf + f⋅Δa]⋅sin(2φ)) / (ρ⋅sin1″) - * Δh = tX⋅cosφ⋅cosλ + tY⋅cosφ⋅sinλ + tZ⋅sinφ + (a⋅Δf + f⋅Δa)⋅sin²φ - Δa - * - * we set: - * - * dfm = (a⋅Δf + f⋅Δa) in abridged case (b⋅Δf in non-abridged case) - * sin(2φ) = 2⋅sin(φ)⋅cos(φ) - */ - final double sinλ = sin(λ); - final double cosλ = cos(λ); - final double sinφ = sin(φ); - final double cosφ = cos(φ); - final double sin2φ = sinφ * sinφ; - final double ν2den = 1 - eccentricitySquared*sin2φ; // Square of the denominator of ν - final double νden = sqrt(ν2den); // Denominator of ν - final double ρden = ν2den * νden; // Denominator of ρ - double ρ = semiMajor * (1 - eccentricitySquared) / ρden; // Other notation: Rm = ρ - double ν = semiMajor / νden; // Other notation: Rn = ν - double t = Δfmod * 2; // A term in the calculation of Δφ - if (!isAbridged) { - ρ += h; - ν += h; - t = t*(0.5/νden + 0.5/ρden) // = Δf⋅[ν⋅(b/a) + ρ⋅(a/b)] (without the +h in ν and ρ) - + Δa*eccentricitySquared/νden; // = Δa⋅[ℯ²⋅ν/a] - } - final double spcλ = tY*sinλ + tX*cosλ; // "spc" stands for "sin plus cos" - final double cmsλ = tY*cosλ - tX*sinλ; // "cms" stands for "cos minus sin" - final double cmsφ = (tZ + t*sinφ)*cosφ - spcλ*sinφ; - final double scaleX = ANGULAR_SCALE / (ν*cosφ); - final double scaleY = ANGULAR_SCALE / ρ; - final double λt = λ + (cmsλ * scaleX); // The target geographic coordinates - final double φt = φ + (cmsφ * scaleY); - if (dstPts != null) { - dstPts[dstOff++] = λt; - dstPts[dstOff++] = φt; - if (isTarget3D) { - double t1 = Δfmod * sin2φ; // A term in the calculation of Δh - double t2 = Δa; - if (!isAbridged) { - t1 /= νden; // = Δf⋅(b/a)⋅ν⋅sin²φ - t2 *= νden; // = Δa⋅(a/ν) - } - dstPts[dstOff++] = h + spcλ*cosφ + tZ*sinφ + t1 - t2; - } - } - if (!derivate) { - return null; - } - /* - * At this point the (Abridged) Molodensky transformation is finished. - * Code below this point is only for computing the derivative, if requested. - * Note: variable names do not necessarily tell all the terms that they contain. - */ - final Matrix matrix = Matrices.createDiagonal(getTargetDimensions(), getSourceDimensions()); - final double sinφcosφ = sinφ * cosφ; - final double dν = eccentricitySquared*sinφcosφ / ν2den; - final double dν3ρ = 3*dν * (1 - eccentricitySquared) / ν2den; - // double dXdλ = spcλ; - final double dYdλ = cmsλ * sinφ; - final double dZdλ = cmsλ * cosφ; - double dXdφ = dYdλ / cosφ; - double dYdφ = -tZ*sinφ - cosφ*spcλ + t*(1 - 2*sin2φ); - double dZdφ = tZ*cosφ - sinφ*spcλ; - if (isAbridged) { - /* - * Δfmod = (a⋅Δf) + (f⋅Δa) - * t = 2⋅Δfmod - * dXdh = 0 so no need to set the matrix element. - * dYdh = 0 so no need to set the matrix element. - */ - dXdφ -= cmsλ * dν; - dYdφ -= cmsφ * dν3ρ; - dZdφ += t*cosφ*sinφ; - } else { - /* - * Δfmod = b⋅Δf - * t = Δf⋅[ν⋅(b/a) + ρ⋅(a/b)] (real ν and ρ, without + h) - * ν is actually ν + h - * ρ is actually ρ + h - */ - final double dρ = dν3ρ * νden * (semiMajor / ρ); // Reminder: that ρ contains a h term. - dXdφ -= dν * cmsλ * semiMajor / (νden*ν); // Reminder: that ν contains a h term. - dYdφ -= dρ * dZdφ - (Δfmod*(dν*2/(1 - eccentricitySquared) + (1 + 1/ν2den)*(dν - dρ)) - + Δa*(dν + 1)*eccentricitySquared) * sinφcosφ / νden; - if (isSource3D) { - final double dXdh = cmsλ / ν; - final double dYdh = -cmsφ / ρ; - matrix.setElement(0, 2, -dXdh * scaleX); - matrix.setElement(1, 2, +dYdh * scaleY); - } - final double t1 = Δfmod * (dν*sin2φ + 2*sinφcosφ); - final double t2 = Δa * dν; - dZdφ += t1/νden + t2*νden; - } - matrix.setElement(0, 0, 1 - spcλ * scaleX); - matrix.setElement(1, 1, 1 + dYdφ * scaleY); - matrix.setElement(0, 1, + dXdφ * scaleX); - matrix.setElement(1, 0, - dYdλ * scaleY); - if (isTarget3D) { - matrix.setElement(2, 0, dZdλ); - matrix.setElement(2, 1, dZdφ); - } - return matrix; - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - */ - @Override - protected int computeHashCode() { - int code = super.computeHashCode() + Long.hashCode( - Double.doubleToLongBits(Δa) - + Double.doubleToLongBits(Δfmod) - + 31 * (Double.doubleToLongBits(tX) - + 31 * (Double.doubleToLongBits(tY) - + 31 * (Double.doubleToLongBits(tZ))))); - if (isAbridged) code = ~code; - return code; - } - - /** - * Compares the specified object with this math transform for equality. - * - * @return {@inheritDoc} - */ - @Override - public boolean equals(final Object object, final ComparisonMode mode) { - if (object == this) { - // Slight optimization - return true; - } - if (super.equals(object, mode)) { - final MolodenskyFormula that = (MolodenskyFormula) object; - return isSource3D == that.isSource3D - && isTarget3D == that.isTarget3D - && isAbridged == that.isAbridged - && Numerics.epsilonEqual(tX, that.tX, mode) - && Numerics.epsilonEqual(tY, that.tY, mode) - && Numerics.epsilonEqual(tZ, that.tZ, mode) - && Numerics.epsilonEqual(Δa, that.Δa, mode) - && Numerics.epsilonEqual(Δfmod, that.Δfmod, mode) - && Numerics.epsilonEqual(semiMajor, that.semiMajor, mode) - && Numerics.epsilonEqual(eccentricitySquared, that.eccentricitySquared, mode); - // No need to compare the contextual parameters since this is done by super-class. - } - return false; - } -} diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java index f8039f182b..7ea6805057 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java @@ -18,18 +18,26 @@ package org.apache.sis.referencing.operation.transform; import java.util.Arrays; import javax.measure.Unit; +import javax.measure.quantity.Length; import org.opengis.util.FactoryException; +import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransformFactory; import org.opengis.referencing.operation.TransformException; +import org.apache.sis.referencing.datum.DatumShiftGrid; +import org.apache.sis.referencing.datum.DefaultEllipsoid; +import org.apache.sis.referencing.operation.matrix.Matrices; import org.apache.sis.referencing.operation.provider.Molodensky; import org.apache.sis.referencing.operation.provider.AbridgedMolodensky; import org.apache.sis.parameter.Parameters; import org.apache.sis.measure.Units; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.Debug; +import org.apache.sis.util.internal.Numerics; import static java.lang.Math.*; @@ -81,14 +89,14 @@ import static java.lang.Math.*; * @author Rueben Schulz (UBC) * @author Martin Desruisseaux (IRD, Geomatys) * @author Rémi Maréchal (Geomatys) - * @version 1.3 + * @version 1.5 * @since 0.7 */ -public class MolodenskyTransform extends MolodenskyFormula { +public class MolodenskyTransform extends DatumShiftTransform { /** * Serial number for inter-operability with different versions. */ - private static final long serialVersionUID = 7206439437113286122L; + private static final long serialVersionUID = 1741638055238886288L; /** * Internal parameter descriptor, used only for debugging purpose. @@ -99,12 +107,93 @@ public class MolodenskyTransform extends MolodenskyFormula { @Debug private static ParameterDescriptorGroup DESCRIPTOR; + /** + * The value of 1/sin(1″) multiplied by the conversion factor from arc-seconds to radians (π/180)/(60⋅60). + * This is the final multiplication factor for Δλ and Δφ. + */ + static final double ANGULAR_SCALE = 1.00000000000391744; + + /** + * {@code true} if the source coordinates have a height. + */ + private final boolean isSource3D; + + /** + * {@code true} if the target coordinates have a height. + */ + private final boolean isTarget3D; + + /** + * {@code true} for the abridged formula, or {@code false} for the complete one. + */ + private final boolean isAbridged; + + /** + * Shift along the geocentric X axis (toward prime meridian) + * in units of the semi-major axis of the source ellipsoid. + * + * @see org.apache.sis.referencing.datum.BursaWolfParameters#tX + */ + protected final double tX; + + /** + * Shift along the geocentric Y axis (toward 90°E) + * in units of the semi-major axis of the source ellipsoid. + * + * @see org.apache.sis.referencing.datum.BursaWolfParameters#tY + */ + protected final double tY; + + /** + * Shift along the geocentric Z axis (toward north pole) + * in units of the semi-major axis of the source ellipsoid. + * + * @see org.apache.sis.referencing.datum.BursaWolfParameters#tZ + */ + protected final double tZ; + + /** + * Difference in the semi-major axes of the target and source ellipsoids: {@code Δa = target a - source a}. + * + * @see DefaultEllipsoid#semiMajorAxisDifference(Ellipsoid) + */ + private final double Δa; + + /** + * Difference between the flattening of the target and source ellipsoids (Δf), opportunistically modified + * with additional terms. The value depends on whether this Molodensky transform is abridged or not: + * + * <ul> + * <li>For Molodensky, this field is set to (b⋅Δf).</li> + * <li>For Abridged Molodensky, this field is set to (a⋅Δf) + (f⋅Δa).</li> + * </ul> + * + * where Δf = <var>target flattening</var> - <var>source flattening</var>. + */ + final double Δfmod; + + /** + * Semi-major axis length (<var>a</var>) of the source ellipsoid. + */ + protected final double semiMajor; + + /** + * The square of eccentricity of the source ellipsoid. + * This can be computed by ℯ² = (a²-b²)/a² where + * <var>a</var> is the <cite>semi-major</cite> axis length and + * <var>b</var> is the <cite>semi-minor</cite> axis length. + * + * @see DefaultEllipsoid#getEccentricitySquared() + */ + protected final double eccentricitySquared; + /** * The inverse of this Molodensky transform. + * Shall be considered final. * * @see #inverse() */ - private final MolodenskyTransform inverse; + private MolodenskyTransform inverse; /** * Creates a Molodensky transform from the specified parameters. @@ -148,8 +237,9 @@ public class MolodenskyTransform extends MolodenskyFormula { final double tX, final double tY, final double tZ, final boolean isAbridged) { - super(source, isSource3D, target, isTarget3D, tX, tY, tZ, null, isAbridged, - isAbridged ? AbridgedMolodensky.PARAMETERS : Molodensky.PARAMETERS); + this(source, isSource3D, target, isTarget3D, tX, tY, tZ, null, isAbridged, + isAbridged ? AbridgedMolodensky.PARAMETERS : Molodensky.PARAMETERS); + if (!isSource3D && !isTarget3D) { if (isAbridged && tX == 0 && tY == 0 && tZ == 0) { inverse = new AbridgedMolodenskyTransform2D(this, source, target); @@ -169,24 +259,109 @@ public class MolodenskyTransform extends MolodenskyFormula { * @param target the target ellipsoid of the given {@code inverse} transform. */ MolodenskyTransform(final MolodenskyTransform inverse, final Ellipsoid source, final Ellipsoid target) { - super(inverse, source, target, inverse.context.getDescriptor()); + this(target, inverse.isTarget3D, source, inverse.isSource3D, + -inverse.tX, -inverse.tY, -inverse.tZ, inverse.grid, + inverse.isAbridged, inverse.context.getDescriptor()); + this.inverse = inverse; } /** - * Invoked by constructor and by {@link #getParameterValues()} for setting all parameters other than axis lengths. + * Creates a Molodensky transform from the specified parameters. + * If a non-null {@code grid} is specified, it is caller's responsibility to verify its validity. * - * @param pg where to set the parameters. - * @param semiMinor ignored. - * @param unit the unit of measurement to declare. - * @param Δf the flattening difference to set, or NaN if this method should fetch that value itself. + * @param source the source ellipsoid. + * @param isSource3D {@code true} if the source coordinates have a height. + * @param target the target ellipsoid. + * @param isTarget3D {@code true} if the target coordinates have a height. + * @param tX the geocentric <var>X</var> translation in same units than the source ellipsoid axes. + * @param tY the geocentric <var>Y</var> translation in same units than the source ellipsoid axes. + * @param tZ the geocentric <var>Z</var> translation in same units than the source ellipsoid axes. + * @param grid interpolation grid in geocentric coordinates, or {@code null} if none. + * @param isAbridged {@code true} for the abridged formula, or {@code false} for the complete one. + * @param descriptor the contextual parameter descriptor. */ - @Override - final void completeParameters(final Parameters pg, final double semiMinor, final Unit<?> unit, double Δf) { + private MolodenskyTransform(final Ellipsoid source, final boolean isSource3D, + final Ellipsoid target, final boolean isTarget3D, + final double tX, final double tY, final double tZ, + final DatumShiftGrid<?,?> grid, final boolean isAbridged, + final ParameterDescriptorGroup descriptor) + { + super(descriptor, isSource3D, isTarget3D, grid); + ArgumentChecks.ensureNonNull("source", source); + ArgumentChecks.ensureNonNull("target", target); + final DefaultEllipsoid src = DefaultEllipsoid.castOrCopy(source); + this.isSource3D = isSource3D; + this.isTarget3D = isTarget3D; + this.isAbridged = isAbridged; + this.semiMajor = src.getSemiMajorAxis(); + this.Δa = src.semiMajorAxisDifference(target); + this.tX = tX; + this.tY = tY; + this.tZ = tZ; + + final double semiMinor = src.getSemiMinorAxis(); + final double Δf = src.flatteningDifference(target); + eccentricitySquared = src.getEccentricitySquared(); + Δfmod = isAbridged ? (semiMajor * Δf) + (semiMajor - semiMinor) * (Δa / semiMajor) + : (semiMinor * Δf); + /* + * Copy parameters to the ContextualParameter. Those parameters are not used directly + * by MolodenskyTransform, but we need to store them in case the user asks for them. + */ + final Unit<Length> unit = src.getAxisUnit(); + setContextParameters(semiMajor, semiMinor, unit, target); + completeParameters(context, unit, Δf); + /* + * Prepare two affine transforms to be executed before and after the MolodenskyTransform: + * + * - A "normalization" transform for converting degrees to radians, + * - A "denormalization" transform for for converting radians to degrees. + */ + context.normalizeGeographicInputs(0); + context.denormalizeGeographicOutputs(0); + } + + /** + * Sets parameter values in the given group for parameters other than axis lengths. + * This method is invoked for both completing contextual parameters ({@code pg == context}) and + * for completing internal parameters ({@code pg != context}). When this method is invoked, the + * following parameters are already set: + * + * <ul> + * <li>{@code "src_semi_major"}</li> + * <li>{@code "src_semi_minor"}</li> + * <li>{@code "tgt_semi_major"} (contextual parameters only)</li> + * <li>{@code "tgt_semi_minor"} (contextual parameters only)</li> + * </ul> + * + * This method sets the following parameters: + * + * <ul> + * <li><q>dim</q></li> + * <li><q>X-axis translation</q> (Molodensky only)</li> + * <li><q>Y-axis translation</q> (Molodensky only)</li> + * <li><q>Z-axis translation</q> (Molodensky only)</li> + * <li><q>Geocentric translation file</q> (Geocentric interpolations only)</li> + * <li><q>Semi-major axis length difference</q> (Always for Molodensky, internal WKT only for geocentric interpolations)</li> + * <li><q>Flattening difference</q> (Always for Molodensky, internal WKT only for geocentric interpolations)</li> + * </ul> + * + * @param pg where to set the parameters. + * @param unit the unit of measurement to declare. + * @param Δf the flattening difference to set, or NaN if this method should fetch that value itself. + */ + private void completeParameters(final Parameters pg, final Unit<?> unit, double Δf) { if (Double.isNaN(Δf)) { Δf = context.doubleValue(Molodensky.FLATTENING_DIFFERENCE); } - super.completeParameters(pg, semiMinor, unit, Δf); + /* + * Unconditionally set the "dim" parameters to the number of source dimensions (do not check for consistency + * with the number of target dimensions) because source dimensions determine the value of ellipsoidal heights, + * which may change the horizontal numerical values. By contrast, the number of target dimensions does not have + * any impact on numerical values (it can just causes a drop of the third value). + */ + pg.getOrCreate(Molodensky.DIMENSION).setValue(getSourceDimensions()); pg.getOrCreate(Molodensky.TX) .setValue(tX, unit); pg.getOrCreate(Molodensky.TY) .setValue(tY, unit); pg.getOrCreate(Molodensky.TZ) .setValue(tZ, unit); @@ -258,21 +433,156 @@ public class MolodenskyTransform extends MolodenskyFormula { return tX == 0 && tY == 0 && tZ == 0 && Δa == 0 && Δfmod == 0 && getSourceDimensions() == getTargetDimensions(); } + /** + * Gets the dimension of input points. + * + * @return the input dimension, which is 2 or 3. + */ + @Override + public final int getSourceDimensions() { + return isSource3D ? 3 : 2; + } + + /** + * Gets the dimension of output points. + * + * @return the output dimension, which is 2 or 3. + */ + @Override + public final int getTargetDimensions() { + return isTarget3D ? 3 : 2; + } + /** * Transforms the (λ,φ) or (λ,φ,<var>h</var>) coordinates between two geographic CRS, * and optionally returns the derivative at that location. * + * @param dstPts the array into which the transformed coordinate is returned, or {@code null}. + * @param srcOff the offset to the point to be transformed in the source array. + * @param dstPts the array into which the transformed coordinates is returned. + * @param dstOff the offset to the location of the transformed point that is stored in the destination array. + * @param derivate {@code true} for computing the derivative, or {@code false} if not needed. * @return {@inheritDoc} * @throws TransformException if the point cannot be transformed or * if a problem occurred while calculating the derivative. */ @Override public Matrix transform(final double[] srcPts, final int srcOff, - final double[] dstPts, final int dstOff, + final double[] dstPts, int dstOff, final boolean derivate) throws TransformException { - return transform(srcPts[srcOff], srcPts[srcOff+1], isSource3D ? srcPts[srcOff+2] : 0, - dstPts, dstOff, tX, tY, tZ, derivate); + final double λ = srcPts[srcOff]; + final double φ = srcPts[srcOff+1]; + final double h = isSource3D ? srcPts[srcOff+2] : 0; + /* + * Abridged Molodensky formulas from EPSG guidance note: + * + * ν = a / √(1 - ℯ²⋅sin²φ) : radius of curvature in the prime vertical + * ρ = a⋅(1 – ℯ²) / (1 – ℯ²⋅sin²φ)^(3/2) : radius of curvature in the meridian + * Δλ″ = (-tX⋅sinλ + tY⋅cosλ) / (ν⋅cosφ⋅sin1″) + * Δφ″ = (-tX⋅sinφ⋅cosλ - tY⋅sinφ⋅sinλ + tZ⋅cosφ + [a⋅Δf + f⋅Δa]⋅sin(2φ)) / (ρ⋅sin1″) + * Δh = tX⋅cosφ⋅cosλ + tY⋅cosφ⋅sinλ + tZ⋅sinφ + (a⋅Δf + f⋅Δa)⋅sin²φ - Δa + * + * we set: + * + * dfm = (a⋅Δf + f⋅Δa) in abridged case (b⋅Δf in non-abridged case) + * sin(2φ) = 2⋅sin(φ)⋅cos(φ) + */ + final double sinλ = sin(λ); + final double cosλ = cos(λ); + final double sinφ = sin(φ); + final double cosφ = cos(φ); + final double sin2φ = sinφ * sinφ; + final double ν2den = 1 - eccentricitySquared*sin2φ; // Square of the denominator of ν + final double νden = sqrt(ν2den); // Denominator of ν + final double ρden = ν2den * νden; // Denominator of ρ + double ρ = semiMajor * (1 - eccentricitySquared) / ρden; // Other notation: Rm = ρ + double ν = semiMajor / νden; // Other notation: Rn = ν + double t = Δfmod * 2; // A term in the calculation of Δφ + if (!isAbridged) { + ρ += h; + ν += h; + t = t*(0.5/νden + 0.5/ρden) // = Δf⋅[ν⋅(b/a) + ρ⋅(a/b)] (without the +h in ν and ρ) + + Δa*eccentricitySquared/νden; // = Δa⋅[ℯ²⋅ν/a] + } + final double spcλ = tY*sinλ + tX*cosλ; // "spc" stands for "sin plus cos" + final double cmsλ = tY*cosλ - tX*sinλ; // "cms" stands for "cos minus sin" + final double cmsφ = (tZ + t*sinφ)*cosφ - spcλ*sinφ; + final double scaleX = ANGULAR_SCALE / (ν*cosφ); + final double scaleY = ANGULAR_SCALE / ρ; + final double λt = λ + (cmsλ * scaleX); // The target geographic coordinates + final double φt = φ + (cmsφ * scaleY); + if (dstPts != null) { + dstPts[dstOff++] = λt; + dstPts[dstOff++] = φt; + if (isTarget3D) { + double t1 = Δfmod * sin2φ; // A term in the calculation of Δh + double t2 = Δa; + if (!isAbridged) { + t1 /= νden; // = Δf⋅(b/a)⋅ν⋅sin²φ + t2 *= νden; // = Δa⋅(a/ν) + } + dstPts[dstOff++] = h + spcλ*cosφ + tZ*sinφ + t1 - t2; + } + } + if (!derivate) { + return null; + } + /* + * At this point the (Abridged) Molodensky transformation is finished. + * Code below this point is only for computing the derivative, if requested. + * Note: variable names do not necessarily tell all the terms that they contain. + */ + final Matrix matrix = Matrices.createDiagonal(getTargetDimensions(), getSourceDimensions()); + final double sinφcosφ = sinφ * cosφ; + final double dν = eccentricitySquared*sinφcosφ / ν2den; + final double dν3ρ = 3*dν * (1 - eccentricitySquared) / ν2den; + // double dXdλ = spcλ; + final double dYdλ = cmsλ * sinφ; + final double dZdλ = cmsλ * cosφ; + double dXdφ = dYdλ / cosφ; + double dYdφ = -tZ*sinφ - cosφ*spcλ + t*(1 - 2*sin2φ); + double dZdφ = tZ*cosφ - sinφ*spcλ; + if (isAbridged) { + /* + * Δfmod = (a⋅Δf) + (f⋅Δa) + * t = 2⋅Δfmod + * dXdh = 0 so no need to set the matrix element. + * dYdh = 0 so no need to set the matrix element. + */ + dXdφ -= cmsλ * dν; + dYdφ -= cmsφ * dν3ρ; + dZdφ += t*cosφ*sinφ; + } else { + /* + * Δfmod = b⋅Δf + * t = Δf⋅[ν⋅(b/a) + ρ⋅(a/b)] (real ν and ρ, without + h) + * ν is actually ν + h + * ρ is actually ρ + h + */ + final double dρ = dν3ρ * νden * (semiMajor / ρ); // Reminder: that ρ contains a h term. + dXdφ -= dν * cmsλ * semiMajor / (νden*ν); // Reminder: that ν contains a h term. + dYdφ -= dρ * dZdφ - (Δfmod*(dν*2/(1 - eccentricitySquared) + (1 + 1/ν2den)*(dν - dρ)) + + Δa*(dν + 1)*eccentricitySquared) * sinφcosφ / νden; + if (isSource3D) { + final double dXdh = cmsλ / ν; + final double dYdh = -cmsφ / ρ; + matrix.setElement(0, 2, -dXdh * scaleX); + matrix.setElement(1, 2, +dYdh * scaleY); + } + final double t1 = Δfmod * (dν*sin2φ + 2*sinφcosφ); + final double t2 = Δa * dν; + dZdφ += t1/νden + t2*νden; + } + matrix.setElement(0, 0, 1 - spcλ * scaleX); + matrix.setElement(1, 1, 1 + dYdφ * scaleY); + matrix.setElement(0, 1, + dXdφ * scaleX); + matrix.setElement(1, 0, - dYdλ * scaleY); + if (isTarget3D) { + matrix.setElement(2, 0, dZdλ); + matrix.setElement(2, 1, dZdφ); + } + return matrix; } /** @@ -319,7 +629,7 @@ public class MolodenskyTransform extends MolodenskyFormula { } /* * The code in the following loop is basically a copy-and-paste of the code in the - * MolodenskyFormula.transform(λ, φ, h, …) method, but without derivative matrix + * MolodenskyTransform.transform(λ, φ, h, …) method, but without derivative matrix * computation and without support for interpolation of (tX,tY,tZ) values in a grid. */ while (--numPts >= 0) { @@ -407,4 +717,74 @@ public class MolodenskyTransform extends MolodenskyFormula { return DESCRIPTOR; } } + + /** + * Returns a copy of internal parameter values of this transform. + * The returned group contains parameters for the source ellipsoid semi-axis lengths + * and the differences between source and target ellipsoid parameters. + * + * <h4>Usage note</h4> + * This method is mostly for {@linkplain org.apache.sis.io.wkt.Convention#INTERNAL debugging purposes} + * since the isolation of non-linear parameters in this class is highly implementation dependent. + * Most GIS applications will instead be interested in the {@linkplain #getContextualParameters() + * contextual parameters}. + * + * @return a copy of the internal parameter values for this transform. + */ + @Debug + @Override + public ParameterValueGroup getParameterValues() { + final Unit<?> unit = context.getOrCreate(Molodensky.SRC_SEMI_MAJOR).getUnit(); + final double semiMinor = context.getOrCreate(Molodensky.SRC_SEMI_MINOR).doubleValue(unit); + final Parameters pg = Parameters.castOrWrap(getParameterDescriptors().createValue()); + pg.getOrCreate(Molodensky.SRC_SEMI_MAJOR).setValue(semiMajor, unit); + pg.getOrCreate(Molodensky.SRC_SEMI_MINOR).setValue(semiMinor, unit); + completeParameters(pg, unit, Double.NaN); + return pg; + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + */ + @Override + protected int computeHashCode() { + int code = super.computeHashCode() + Long.hashCode( + Double.doubleToLongBits(Δa) + + Double.doubleToLongBits(Δfmod) + + 31 * (Double.doubleToLongBits(tX) + + 31 * (Double.doubleToLongBits(tY) + + 31 * (Double.doubleToLongBits(tZ))))); + if (isAbridged) code = ~code; + return code; + } + + /** + * Compares the specified object with this math transform for equality. + * + * @return {@inheritDoc} + */ + @Override + public boolean equals(final Object object, final ComparisonMode mode) { + if (object == this) { + // Slight optimization + return true; + } + if (super.equals(object, mode)) { + final MolodenskyTransform that = (MolodenskyTransform) object; + return isSource3D == that.isSource3D + && isTarget3D == that.isTarget3D + && isAbridged == that.isAbridged + && Numerics.epsilonEqual(tX, that.tX, mode) + && Numerics.epsilonEqual(tY, that.tY, mode) + && Numerics.epsilonEqual(tZ, that.tZ, mode) + && Numerics.epsilonEqual(Δa, that.Δa, mode) + && Numerics.epsilonEqual(Δfmod, that.Δfmod, mode) + && Numerics.epsilonEqual(semiMajor, that.semiMajor, mode) + && Numerics.epsilonEqual(eccentricitySquared, that.eccentricitySquared, mode); + // No need to compare the contextual parameters since this is done by super-class. + } + return false; + } }