blob: 5af8a7094ef68d57c9523b1f1031991fc38fa2b7 [file] [log] [blame]
/*
* 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;
import org.opengis.geometry.coordinate.Position;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.math.MathFunctions;
import static java.lang.Math.*;
/**
* Performs geodetic calculations on an ellipsoid. This class overrides the spherical
* formulas implemented in the parent class, replacing them by ellipsoidal formulas.
* The methods for direct and inverse geodesic problem use the formulas described in
* the following publication:
*
* <blockquote>
* Charles F. F. Karney, 2013.
* <a href="https://doi.org/10.1007/s00190-012-0578-z">Algorithms for geodesics</a>, SRI International.
* </blockquote>
*
* The following symbols are used with the same meaning than in Karney's article,
* except λ₁₂ which is represented by ∆λ:
*
* <ul>
* <li><var>a</var>: equatorial radius of the ellipsoid of revolution.</li>
* <li><var>b</var>: polar semi-axis of the ellipsoid of revolution.</li>
* <li><var>ℯ</var>: first eccentricity: ℯ = √[(a²-b²)/a²].</li>
* <li><var>ℯ′</var>: second eccentricity: ℯ′ = √[(a²-b²)/b²].</li>
* <li><var>n</var>: third flattening: n = (a-b)/(a+b).</li>
* <li><var>E</var>: the point at which the geodesic crosses the equator in the northward direction.</li>
* <li><var>P</var>: the start point (P₁) or end point (P₂).</li>
* <li><var>∆λ</var>: longitude difference between start point and end point (λ₁₂ in Karney).</li>
* <li><var>β</var>: reduced latitude, related to φ geodetic latitude on the ellipsoid.</li>
* <li><var>ω</var>: spherical longitude, related to λ geodetic longitude on the ellipsoid.</li>
* <li><var>σ</var>: spherical arc length, related to distance <var>s</var> on the ellipsoid.</li>
* </ul>
*
* Suffix 1 in variable names denotes values computed using starting point (P₁) and starting azimuth (α₁)
* while suffix 2 denotes values computed using ending point (P₂) and ending azimuth (α₂).
* All angular values stored in this class are in radians.
*
* <p><b>Limitations:</b>
* Current implementation is still unable to compute the geodesics in some cases.
* In particular, calculation may fail for antipodal points.
* See <a href="https://issues.apache.org/jira/browse/SIS-467">SIS-467</a>.</p>
*
* <p>If the following cases where more than one geodesics exist, current implementation returns an arbitrary one:</p>
* <ul>
* <li>Coincident points (distance is zero but azimuths can be anything).</li>
* <li>Starting point and ending points are at opposite poles (there are infinitely many geodesics).</li>
* <li>φ₁ = -φ₂ and ∆λ is close to 180° (two geodesics may exist).</li>
* <li>∆λ = ±180° (two geodesics may exist).</li>
* </ul>
*
* @author Matthieu Bastianelli (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*/
class GeodesicsOnEllipsoid extends GeodeticCalculator {
/**
* Whether to include code used for JUnit tests only. This field should be
* set to {@code true} during development and to {@code false} in releases.
*
* @see #snapshot()
*/
static final boolean STORE_LOCAL_VARIABLES = false;
/**
* Accuracy threshold iterative computations, in radians.
* We take a finer accuracy than default SIS configuration in order to met the accuracy of numbers
* published in Karney (2013). If this value is modified, the effect can be verified by executing
* the {@code GeodesicsOnEllipsoidTest} methods that compare computed values against Karney's tables.
* Remember to update {@link GeodeticCalculator} class javadoc if this value is changed.
*
* <p><b>Note:</b> when the iteration loop detects that it reached this requested accuracy, the loop
* completes the iteration step which was in progress. Consequently the final accuracy is one iteration
* better than the accuracy computed from this value.</p>
*/
static final double ITERATION_TOLERANCE = Formulas.ANGULAR_TOLERANCE * (PI/180) / 20;
/**
* Difference between ending point and antipode of starting point for considering them as nearly antipodal.
* This is used only for finding an approximate position before to start iteration using Newton's method,
* so it is okay if the antipodal approximation has some inaccuracy.
*/
private static final double NEARLY_ANTIPODAL_Δλ = 0.25 * (PI/180);
/**
* The square of eccentricity: ℯ² = (a²-b²)/a² where
* <var>ℯ</var> is the eccentricity,
* <var>a</var> is the <cite>semi-major</cite> axis length and
* <var>b</var> is the <cite>semi-minor</cite> axis length.
*/
final double eccentricitySquared;
/**
* The square of the second eccentricity: ℯ′² = (a²-b²)/b².
*/
final double secondEccentricitySquared;
/**
* Third flattening of the ellipsoid: n = (a-b)/(a+b).
* Not to be confused with the third eccentricity squared ℯ″² = (a²-b²)/(a²+b²) which is not used in this class.
*/
final double thirdFlattening;
/**
* Ration between the semi-minor and semi-major axis b/a.
* This is related to flattening <var>f</var> = 1 - b/a.
*/
final double axisRatio;
/**
* Length of semi-minor axis.
*
* @see org.opengis.referencing.datum.Ellipsoid#getSemiMinorAxis()
*/
private double semiMinorAxis() {
return semiMajorAxis * axisRatio;
}
/**
* The α value computed from the starting point and starting azimuth.
* We use the sine and cosine instead than the angles because those components are more frequently used than angles.
* Those values can be kept constant when computing many end points and end azimuths at different geodesic distances.
* The {@link #COEFFICIENTS_FOR_START_POINT} flag specifies whether those fields need to be recomputed.
*/
private double sinα0, cosα0;
/**
* Longitude angle from the equatorial point <var>E</var> to starting point P₁ on the ellipsoid.
* This longitude is computed from the ω₁ longitude on auxiliary sphere, which is itself computed
* from α₀, α₁, β₁ and ω₁ values computed from the starting point and starting azimuth.
* The {@link #COEFFICIENTS_FOR_START_POINT} flag specifies whether this field needs to be recomputed.
*
* @see #sphericalToGeodeticLongitude(double, double)
*/
private double λ1E;
/**
* Ellipsoidal arc length <var>s₁</var>/<var>b</var> computed from the spherical arc length <var>σ₁</var>.
* The <var>σ₁</var> value is an arc length on the auxiliary sphere between equatorial point <var>E</var>
* (the point on equator with forward direction toward azimuth α₀) and starting point P₁. This is computed
* by I₁(σ₁) in Karney equation 15 and is saved for reuse when the starting point and azimuth do not change.
* The {@link #COEFFICIENTS_FOR_START_POINT} flag specifies whether this field needs to be recomputed.
*
* @see #sphericalToEllipsoidalAngle(double, boolean)
*/
private double I1_σ1;
/**
* The term to be raised to powers (ε⁰, ε¹, ε², ε³, …) in series expansions. Defined in Karney equation 16 as
* ε = (√[k²+1] - 1) / (√[k²+1] + 1) where k² = {@linkplain #secondEccentricitySquared ℯ′²}⋅cos²α₀ (Karney 9).
* The {@link #COEFFICIENTS_FOR_START_POINT} flag specifies whether this field needs to be recomputed.
*/
private double ε;
/**
* Coefficients in series expansion of the form A × (σ + ∑Cₓ⋅sin(…⋅σ)).
* There is 3 series expansions used in this class:
*
* <ul>
* <li><var>A₁</var> and <var>C₁ₓ</var> (Karney 15) are used for arc length conversions from auxiliary sphere to ellipsoid.</li>
* <li><var>A₂</var> and <var>C₂ₓ</var> (Karney 41) are used in calculation of reduced length.</li>
* <li><var>A₃</var> and <var>C₃ₓ</var> (Karney 23) are used for calculation of geodetic longitude from spherical angles.</li>
* </ul>
*
* The <var>Cₓₓ</var> coefficients are hard-coded, except the <var>C₃ₓ</var> coefficients
* (used together with <var>A₃</var>) which depend on {@link #sinα0} and {@link #cosα0}.
* Note that the <var>C</var> coefficients differ from the ones published by Karney because
* they have been combined using Clenshaw summation.
*
* <p>All those coefficients must be recomputed when the starting point or starting azimuth change.
* The {@link #COEFFICIENTS_FOR_START_POINT} flag specifies whether those fields need to be recomputed.</p>
*
* @see #computeSeriesExpansionCoefficients()
*/
private double A1, A2, A3, C31, C32, C33, C34, C35;
/**
* Coefficients for Rhumb line calculation from Bennett (1996) equation 2, modified with Clenshaw summation.
*/
private final double R0, R2, R4, R6;
/**
* Constructs a new geodetic calculator expecting coordinates in the supplied CRS.
*
* @param crs the referencing system for the {@link Position} arguments and return values.
* @param ellipsoid ellipsoid associated to the geodetic component of given CRS.
*/
GeodesicsOnEllipsoid(final CoordinateReferenceSystem crs, final Ellipsoid ellipsoid) {
super(crs, ellipsoid);
final double a = semiMajorAxis;
final double b = ellipsoid.getSemiMinorAxis();
final double Δ2 = a*a - b*b;
eccentricitySquared = Δ2 / (a*a);
secondEccentricitySquared = Δ2 / (b*b);
thirdFlattening = (a - b) / (a + b);
axisRatio = b / a;
/*
* For rhumb-line calculation from Bennett (1996) equation 2, modified with Clenshaw summation.
*/
double fe;
R0 = 1 - (eccentricitySquared)*( 1./4 + eccentricitySquared*( 3./64 + eccentricitySquared*( 5./256)));
R2 = (fe = eccentricitySquared)*( -3./8 - eccentricitySquared*( 3./32 + eccentricitySquared*(25./768)));
R4 = (fe *= eccentricitySquared)*( 15./128 + eccentricitySquared*(45./512));
R6 = (fe * eccentricitySquared)*(-35./768);
}
/**
* Computes series expansions coefficients.
*
* <p><b>Preconditions:</b> The {@link #sinα0} and {@link #cosα0} fields shall be set before to invoke this method.
* It is caller's responsibility to ensure that sin(α₀)² + cos(α₀)² ≈ 1 (this is verified in assertion).</p>
*
* <p><b>Post-conditions:</b> this method sets the {@link #ε}, {@link #A1}, {@link #A2}, {@link #A3},
* {@link #C31}, {@link #C32}, {@link #C33}, {@link #C34}, {@link #C35} fields.</p>
*
* It is caller's responsibility to invoke {@code setValid(COEFFICIENTS_FOR_START_POINT)} after it has updated
* {@link #λ1E} and {@link #I1_σ1}.
*
* @return k² = ℯ′²⋅cos²α₀ term (Karney 9).
*/
private double computeSeriesExpansionCoefficients() {
assert abs(sinα0*sinα0 + cosα0*cosα0 - 1) <= 1E-10; // Arbitrary threshold.
final double k2 = secondEccentricitySquared * (cosα0 * cosα0); // (Karney 9)
final double h = sqrt(1 + k2);
ε = (h - 1) / (h + 1); // (Karney 16)
final double ε2 = ε*ε;
A1 = (((( 1./256)*ε2 + 1./64)*ε2 + 1./4)*ε2 + 1) / (1 - ε); // (Karney 17)
A2 = ((((25./256)*ε2 + 9./64)*ε2 + 1./4)*ε2 + 1) * (1 - ε); // (Karney 42)
/*
* Compute A₃ from Karney equation 24 with signs redistributed in a different but equivalent way.
* We use + or - operations in a way where multiplication factors of n are positive. By increasing
* the number of times where the same value is used (for example a single 1/8 instead of two values
* +1/8 and -1/8), our hope is that the compiler can reuse more often values that are already loaded
* in registers. Maybe some compilers could even detect when the same multiplication is done two times.
*
* Note: for each line below, we could compute at construction time the parts at the right of ε.
* However since they are only a few additions and multiplications, it may not be worth to vastly
* increase the number of fields in `GeodesicOnEllipsoid` for that purpose, especially if compiler
* can optimize itself those duplicated multiplications.
*/
final double n = thirdFlattening;
A3 = 1 - ε*(1./2 - n*(1./2)
+ ε*(1./4 + n*(1./8 - n*(3./8 ))
+ ε*(1./16 + n*(3./16 + n*(1./16))
+ ε*(3./64 + n*(1./32)
+ ε*(3./128)))));
/*
* Karney equation 25 with fε = εⁿ where n = 1, 2, 3, …. The coefficients below differ from
* the ones published by Karney because they have been combined using Clenshaw summation
* (see sphericalToEllipsoidalAngle(…) below for a simpler example for Clenshaw summation).
*/
double fε;
C31 = (fε = ε) * ( 1./4 - n*(1./4)
+ ε * ( 1./8 - n*( n*(1./8 ))
+ ε * ( 1./48 + n*(3./32 - n*(1./24))
+ ε * ( 1./64 + n*(1./24)
+ ε * (23./1280)))));
C32 = (fε *= ε) * ( 1./8 - n*(3./16 - n*(1./16))
+ ε * ( 3./32 - n*(1./16 + n*(3./32))
+ ε * (-1./128 + n*(1./8)
+ ε * (-1./64))));
C33 = (fε *= ε) * ( 5./48 - n*(3./16 - n*(5./48))
+ ε * ( 3./32 - n*(5./48)
+ ε * (-7./160)));
C34 = (fε *= ε) * ( 7./64 - n*(7./32)
+ ε * ( 7./64));
C35 = (fε * ε) * (21./160);
return k2;
}
/**
* Computes a term in calculation of ellipsoidal arc length <var>s</var> from the given spherical arc length <var>σ</var>.
* The <var>σ</var> value is an arc length on the auxiliary sphere between equatorial point <var>E</var>
* (the point on equator with forward direction toward azimuth α₀) and another point.
*
* <p>If the {@code minusI2} argument is {@code false}, then this method computes the ellipsoidal arc length <var>s</var>
* from the given spherical arc length <var>σ</var>. This value is computed by Karney's equation 7:
*
* <blockquote>s/b = I₁(σ)</blockquote>
*
* If the {@code minusI2} argument is {@code true}, then this method is modified for computing
* Karney's equation 40 instead (a term in calculation of reduced length <var>m</var>):
*
* <blockquote>J(σ) = I₁(σ) - I₂(σ)</blockquote>
*
* <p><b>Precondition:</b> {@link #computeSeriesExpansionCoefficients()} must have been invoked before this method.</p>
*
* @param σ arc length on the auxiliary sphere since equatorial point <var>E</var>.
* @param minusI2 whether to subtract I₂(σ) after calculation of I₁(σ).
* @return I₁(σ) if {@code minusI2} is false, or J(σ) if {@code minusI2} is true.
*/
private double sphericalToEllipsoidalAngle(final double σ, final boolean minusI2) {
/*
* Derived from Karney (2013) equation 18 with θ = 2σ:
*
* ∑Cₓ⋅sin(x⋅θ) ≈ (-1/2 ⋅ε + 3/16 ⋅ε³ + -1/32 ⋅ε⁵)⋅sin(1θ) + // C₁₁
* (-1/16⋅ε² + 1/32 ⋅ε⁴ + -9/2048⋅ε⁶)⋅sin(2θ) + // C₁₂
* ( -1/48 ⋅ε³ + 3/256 ⋅ε⁵)⋅sin(3θ) + // C₁₃
* ( -5/512⋅ε⁴ + 3/512 ⋅ε⁶)⋅sin(4θ) + // C₁₄
* ( -7/1280⋅ε⁵)⋅sin(5θ) + // C₁₅
* ( -7/2048⋅ε⁶)⋅sin(6θ) // C₁₆
*
* For performance and simplicity, we rewrite the equations using trigonometric identities
* as described in Snyder 3-34 and 3-35, expanded with two more terms. Given:
*
* s = A⋅sin(θ) + B⋅sin(2θ) + C⋅sin(3θ) + D⋅sin(4θ) + E⋅sin(5θ) + F⋅sin(6θ)
*
* We rewrite as:
*
* s = sinθ⋅(A′ + cosθ⋅(B′ + cosθ⋅(C′ + cosθ⋅(D′ + cosθ⋅(E′ + cosθ⋅F′)))))
* A′ = A - C + E
* B′ = 2B - 4D + 6F
* C′ = 4C - 12E
* D′ = 8D - 32F
* E′ = 16E
* F′ = 32F
*
* See https://svn.apache.org/repos/asf/sis/analysis/Map%20projection%20formulas.ods for calculations.
* Additions are done with smallest values first for better precision.
*
* See Karney 2011, Geodesics on an ellipsoid of revolution, https://arxiv.org/pdf/1102.1215.pdf
* if more coefficients (up to ε⁸) are desired. Those coefficients may be needed in a future SIS
* version if we want to support celestial bodies with higher flattening factor.
* Issue tracker: https://issues.apache.org/jira/browse/SIS-465
*/
final double ε2 = ε*ε;
final double ε3 = ε*ε2;
final double ε4 = ε22;
final double ε5 = ε23;
final double ε6 = ε33;
final double θ = σ * 2;
final double sinθ = sin(θ);
final double cosθ = cos(θ);
double m = A1 * + sinθ * (-31./640 * ε5 + 5./24 * ε3 + -1./2 * ε
+ cosθ * (-27./512 * ε6 + 13./128 * ε4 + -1./8 * ε2
+ cosθ * ( 9./80 * ε5 + -1./12 * ε3
+ cosθ * ( 5./32 * ε6 + -5./64 * ε4
+ cosθ * ( -7./80 * ε5
+ cosθ * ( -7./64 * ε6)))))));
if (minusI2) {
m -= A2 * + sinθ * ( 39./640 * ε5 + -1./24 * ε3 + 1./2 * ε
+ cosθ * (105./512 * ε6 + -27./128 * ε4 + 3./8 * ε2
+ cosθ * ( -41./80 * ε5 + 5./12 * ε3
+ cosθ * ( -35./32 * ε6 + 35./64 * ε4
+ cosθ * ( 63./80 * ε5
+ cosθ * ( 77./64 * ε6)))))));
}
return m;
}
/**
* Computes the spherical arc length <var>σ</var> from the an ellipsoidal arc length <var>s</var>.
* This method is the converse of {@link #sphericalToEllipsoidalAngle(double, boolean)}.
* Input is τ = s/(b⋅A₁).
*
* <p><b>Precondition:</b> {@link #computeSeriesExpansionCoefficients()} must have been invoked before this method.</p>
*
* @param τ arc length on the ellipsoid since equatorial point <var>E</var> divided by {@link #A1}.
* @return arc length <var>σ</var> on the auxiliary sphere.
*
* @see #sphericalToEllipsoidalAngle(double, boolean)
*/
private double ellipsoidalToSphericalAngle(final double τ) {
assert !isInvalid(COEFFICIENTS_FOR_START_POINT);
/*
* Derived from Karney (2013) equation 21 with θ = 2τ:
*
* ∑C′ₓ⋅sin(x⋅θ) ≈ (1/2 ⋅ε + -9/32 ⋅ε³ + 205/1536 ⋅ε⁵)⋅sin(1θ) + // C′₁₁
* (5/16⋅ε² + -37/96 ⋅ε⁴ + 1335/4096 ⋅ε⁶)⋅sin(2θ) + // C′₁₂
* ( 29/96 ⋅ε³ + -75/128 ⋅ε⁵)⋅sin(3θ) + // C′₁₃
* ( 539/1536⋅ε⁴ + -2391/2560 ⋅ε⁶)⋅sin(4θ) + // C′₁₄
* ( 3467/7680 ⋅ε⁵)⋅sin(5θ) + // C′₁₅
* ( 38081/61440⋅ε⁶)⋅sin(6θ) // C′₁₆
*
* For performance and simplicity, we rewrite the equations using trigonometric identities
* using the same technic than the one documented above in sphericalToEllipsoidalAngle(σ).
*/
final double ε2 = ε*ε;
final double ε3 = ε*ε2;
final double ε4 = ε22;
final double ε5 = ε23;
final double ε6 = ε33;
final double θ = τ * 2;
final double cosθ = cos(θ);
return τ + sin(θ) * ( 281./240 * ε5 + -7./12 * ε3 + 1./2 * ε
+ cosθ * ( 20753./2560 * ε6 + -835./384 * ε4 + 5./8 * ε2
+ cosθ * ( -4967./640 * ε5 + 29./24 * ε3
+ cosθ * (-52427./1920 * ε6 + 539./192 * ε4
+ cosθ * ( 3467./480 * ε5
+ cosθ * ( 38081./1920 * ε6))))));
}
/**
* Computes the longitude angle λ from the equatorial point <var>E</var> to a point on the ellipsoid
* specified by the ω longitude on auxiliary sphere.
*
* <p><b>Precondition:</b> {@link #computeSeriesExpansionCoefficients()} must have been invoked before this method.</p>
*
* @param ω longitude on the auxiliary sphere.
* @param σ spherical arc length from equatorial point E to point on auxiliary sphere
* along a geodesic with azimuth α₀ at equator.
* @return geodetic longitude λ from the equatorial point E to the point.
*/
private double sphericalToGeodeticLongitude(final double ω, final double σ) {
/*
* Derived from Karney (2013) equation 23:
*
* I₃(σ) = A₃⋅(σ + ∑C₃ₓ⋅sin(x⋅θ))
*
* but with the sum of sine terms replaced by Clenshaw summation.
*/
final double θ = σ * 2;
final double cosθ = cos(θ);
final double I3 = A3*(sin(θ)*(C31 + cosθ*(C32 + cosθ*(C33 + cosθ*(C34 + cosθ*C35)))) + σ);
if (STORE_LOCAL_VARIABLES) store("I₃(σ)", I3);
return ω - sinα0 * I3 * (1 - axisRatio);
}
/**
* Sets the {@link #sinα0} and {@link #cosα0} terms. Note that the {@link #msinα2} field is always set
* to the {@code sinα0} value in this class. But those two fields may nevertheless have different values
* if {@code msinα2} has been set independently, for example by {@link #createGeodesicPath2D(double)}.
*/
private void α0(final double sinα1, final double cosα1, final double sinβ1, final double cosβ1) {
sinα0 = sinα1 * cosβ1; // Azimuth at equator (Karney 5) as geographic angle.
cosα0 = hypot(cosα1, sinα1*sinβ1); // = sqrt(1 - sinα0*sinα0) with less rounding errors.
}
/**
* Computes the end point from the start point, the azimuth and the geodesic distance.
* This method should be invoked if the end point or ending azimuth is requested while
* {@link #END_POINT} validity flag is not set.
*
* <p>This implementation computes {@link #φ2}, {@link #λ2} and azimuths using the method
* described in Karney (2013) <cite>Algorithms for geodesics</cite>. The coefficients of
* Fourier and Taylor series expansions are given by equations 15 and 17.</p>
*
* @throws IllegalStateException if the start point, azimuth or distance has not been set.
*/
@Override
final void computeEndPoint() {
canComputeEndPoint(); // May throw IllegalStateException.
if (isInvalid(COEFFICIENTS_FOR_START_POINT)) {
final double m = hypot(msinα1, mcosα1);
final double sinα1 = msinα1 / m; // α is a geographic (not arithmetic) angle.
final double cosα1 = mcosα1 / m;
final double tanβ1 = axisRatio * tan1); // β₁ is reduced latitude (Karney 6).
final double cosβ1 = 1 / sqrt(1 + tanβ1*tanβ1);
final double sinβ1 = tanβ1 * cosβ1;
α0(sinα1, cosα1, sinβ1, cosβ1);
/*
* Note: Karney said that for equatorial geodesics (φ₁=0 and α₁=π/2), calculation of σ₁ is indeterminate
* and σ₁=0 should be taken. The indetermination appears as atan2(0,0). However this expression evaluates
* to 0 in Java, as required by Math.atan2(y,x) specification, which is what we want. So there is no need
* for a special case.
*/
final double cosα1_cosβ1 = cosα1*cosβ1;
final double σ1 = atan2(sinβ1, cosα1_cosβ1); // Arc length on great circle (Karney 11).
final double ω1 = atan2(sinβ1*sinα0, cosα1_cosβ1);
final double k2 = computeSeriesExpansionCoefficients();
λ1E = sphericalToGeodeticLongitude1, σ1);
I1_σ1 = sphericalToEllipsoidalAngle1, false);
setValid(COEFFICIENTS_FOR_START_POINT);
if (STORE_LOCAL_VARIABLES) { // For comparing values with Karney table 2.
store("β₁", atan2(sinβ1, cosβ1));
store("σ₁", σ1);
store("ω₁", ω1);
store("k²", k2);
snapshot();
}
}
/*
* Distance from equatorial point E to ending point P₂ along the geodesic: s₂ = s₁ + ∆s.
*/
final double s2b = I1_σ1 + geodesicDistance / semiMinorAxis(); // (Karney 18) + ∆s/b
final double σ2 = ellipsoidalToSphericalAngle(s2b / A1); // (Karney 21)
final double sinσ2 = sin2);
final double cosσ2 = cos2);
/*
* Azimuth at ending point α₂ = atan2(sinα₀, cosα₀⋅cosσ₂) (Karney 14 adapted for ending point).
* We do not need atan2(y,x) because we keep x and y components separated. It is not necessary to
* normalize to a vector of length 1.
*/
msinα2 = sinα0;
mcosα2 = cosα0 * cosσ2;
/*
* Ending point coordinates on auxiliary sphere: Latitude β is given by Karney equation 13:
*
* β₂ = atan2(cos(α₀)⋅sin(σ₂), hypot(cos(α₀)⋅cos(σ₂), sin(α₀))
*
* We replace cos(α₀)⋅cos(σ₂) by mcosα2 since we computed it above. Then we avoid the call to
* atan2(y,x) by storing directly the y and x values. Note that `sinβ2` and `cosβ2` are not
* really sine and cosine since we do not normalize them to sin² + cos² = 1. We do not need
* to normalize because we use either a ratio of those 2 quantities or give them to atan2(…).
*/
final double sinβ2 = cosα0 * sinσ2;
final double cosβ2 = hypot(msinα2, mcosα2); // m⋅sin(α₂) = sin(α₀) in this class.
final double ω2 = atan2(sinα0*sinσ2, cosσ2); // (Karney 12).
/*
* Convert reduced longitude ω and latitude β on auxiliary sphere
* to geodetic longitude λ and latitude φ.
*/
final double λ2E = sphericalToGeodeticLongitude2, σ2);
λ2 = IEEEremainder2E - λ1E + λ1, 2*PI);
φ2 = atan(sinβ2 / (cosβ2 * axisRatio)); // (Karney 6).
setValid(END_POINT | ENDING_AZIMUTH);
if (STORE_LOCAL_VARIABLES) { // For comparing values with Karney table 2.
store("s₂", s2b * semiMinorAxis());
store("τ₂", s2b / A1);
store("σ₂", σ2);
store("α₂", atan2(msinα2, mcosα2));
store("β₂", atan2(sinβ2, cosβ2));
store("ω₂", ω2);
store("λ₂", λ2E);
store("Δλ", λ2E - λ1E);
}
}
/**
* Computes the geodesic distance and azimuths from the start point and end point.
* This method should be invoked if the distance or an azimuth is requested while
* {@link #STARTING_AZIMUTH}, {@link #ENDING_AZIMUTH} or {@link #GEODESIC_DISTANCE}
* validity flag is not set.
*
* <p>Reminder: given <var>P₁</var> the starting point and <var>E</var> the intersection of the geodesic with equator:</p>
* <ul>
* <li><var>α₁</var> is the azimuth (0° oriented North) of the geodesic from <var>E</var> to <var>P₁</var>.</li>
* <li><var>σ₁</var> is the spherical arc length (in radians) between <var>E</var> and <var>P₁</var>.</li>
* <li><var>ω₁</var> is the spherical longitude (in radians) on the auxiliary sphere at <var>P₁</var>.
* Spherical longitude is the angle formed by the meridian of <var>E</var> and the meridian of <var>P₁</var>.</li>
* </ul>
*
* @throws IllegalStateException if the distance or azimuth has not been set.
* @throws GeodeticException if an azimuth or the distance can not be computed.
*/
@Override
final void computeDistance() {
canComputeDistance();
/*
* The algorithm in this method requires the following canonical configuration:
*
* Negative latitude of starting point: φ₁ ≦ 0
* Ending point latitude smaller in magnitude: φ₁ ≦ φ₂ ≦ -φ₁
* Positive longitude difference: 0 ≦ ∆λ ≦ π
* (Consequence of above): 0 ≦ α₀ ≦ π/2
*
* If the given points do not met above conditions, then we need to swap start and end points or to
* swap coordinate signs. We apply those changes on local variables only, not on the class fields.
* We will need to apply the converse of those changes in the final results.
*/
double φ1 = this1;
double φ2 = this2;
double Δλ = IEEEremainder2 - λ1, 2*PI); // In [-π … +π] range.
final boolean swapPoints = abs2) > abs1);
if (swapPoints) {
φ1 = this2;
φ2 = this1;
Δλ = -Δλ;
}
final boolean inverseLatitudeSigns = φ1 > 0;
if (inverseLatitudeSigns) {
φ1 = 1;
φ2 = 2;
}
final boolean inverseLongitudeSigns = Δλ < 0;
if (inverseLongitudeSigns) {
Δλ = -Δλ;
}
/*
* Compute an approximation of the azimuth α₁ at starting point. This estimation will be refined by iteration
* in the loop later, but that iteration will not converge if the first α₁ estimation is not good enough. The
* general formula does not give good α₁ initial value for antipodal points, so we need to check for special
* cases first:
*
* 1) Nearly antipodal points with φ₁ = -φ₂.
* 2) Nearly antipodal points with φ₁ slightly different than -φ₂.
* 3) Equatorial case: φ₁ = φ₂ = 0 but restricted to ∆λ ≤ (1-f)⋅π.
* 4) Meridional case: ∆λ = 0 or ∆λ = π (handled by general case in this method).
*/
if 1 > -LATITUDE_THRESHOLD) { // Sufficient test because φ₁ ≦ 0 and |φ₂| ≦ φ₁
/*
* Points on equator but not nearly anti-podal. The geodesic is an arc on equator and the azimuths
* are α₁ = α₂ = ±90°. We need this special case because when φ = 0, the general case get sinβ = 0
* then σ = atan2(sinβ, cosα⋅cosβ) = 0, which result in a distance of 0. I have not yet understood
* how to use the general formulas in such case. This code is a workaround in the meantime.
*
* See https://issues.apache.org/jira/browse/SIS-467
*/
if (Δλ > axisRatio * PI) {
// Karney's special case documented before equation 45.
throw new GeodeticException("Can not compute geodesics for antipodal points on equator.");
}
super.computeDistance();
return;
}
/*
* Reduced latitudes β (Karney 6). Actually we don't need the β angles
* (except for a special case), but rather their sine and cosine values.
*/
final double tanβ1 = axisRatio * tan1);
final double tanβ2 = axisRatio * tan2);
final double cosβ1 = 1 / sqrt(1 + tanβ1*tanβ1);
final double cosβ2 = 1 / sqrt(1 + tanβ2*tanβ2);
final double sinβ1 = tanβ1 * cosβ1;
final double sinβ2 = tanβ2 * cosβ2;
double α1;
if (Δλ >= PI - NEARLY_ANTIPODAL_Δλ && abs1 + φ2) <= NEARLY_ANTIPODAL_Δλ) {
/*
* Nearly antipodal points. Karney's equations 53 are reproduced on the left side
* (with f = 1 - b/a), which we replace by the equations on the right side below:
*
* Δ = f⋅a⋅π⋅cos²β₁ ┃ Δ₁ = f⋅π⋅cosβ₁
* ∆λ = π + Δ/(a⋅cosβ₁)⋅x ┃ ∆λ = π + Δ₁⋅x
* β₂ = -β₁ + (Δ/a)⋅y ┃ β₂ = -β₁ + (Δ₁⋅cosβ₁)⋅y
*/
final double β1 = atan2(sinβ1, cosβ1);
final double β2 = atan2(sinβ2, cosβ2);
final double Δ1 = (1 - axisRatio) * PI * cosβ1; // Differ from Karney by a⋅cosβ₁ factor.
final double y = 2 + β1) / 1*cosβ1);
final double x = (PI - Δλ) / Δ1; // Opposite sign of Karney. We have x ≧ 0.
final double x2 = x*x;
final double y2 = y*y;
if (STORE_LOCAL_VARIABLES) { // For comparing values with Karney table 4.
store("x", -x); // Sign used by Karney.
store("y", y);
}
if (y2 < 1E-12) { // Empirical threshold. See μ(…) for more information.
α1 = (x2 > 1) ? PI/2 : atan(x / sqrt(1 - x2)); // (Karney 57) with opposite sign of x. Result in α₁ ≧ 0.
} else {
final double μ = μ(x2, y2);
α1 = atan2(x*μ, (y*(1+μ))); // (Karney 56) with opposite sign of x.
if (STORE_LOCAL_VARIABLES) {
store("μ", μ);
}
}
} else {
/*
* Usual case (non-antipodal). Estimation is based on variation of geodetic longitude λ
* and spherical longitude ω on the auxiliary sphere. Karney makes a special case for
* Δλ = 0 and Δλ = π by defining α₁ = Δλ. However formulas below produce the same result.
*/
double w = (cosβ1 + cosβ2) / 2;
w = sqrt(1 - eccentricitySquared * (w*w));
final double Δω = Δλ / w;
final double cω = cos(Δω);
final double sω = sin(Δω);
final double αx = cosβ1*sinβ2 - sinβ1*cosβ2*cω;
final double αy = cosβ2*sω;
α1 = atan2y, αx); // (Karney 49)
if (STORE_LOCAL_VARIABLES) { // For comparing values with Karney table 3.
store("ωb", w);
store("Δω", Δω);
store("α₂", atan2(cosβ1*sω, cosβ1*cω*sinβ2 - sinβ1*cosβ2)); // (Karney 50)
store("Δσ", atan2(hypoty, αx), cosβ1*cω*cosβ2 + sinβ1*sinβ2)); // (Karney 51)
// For small distances we could stop here. But it is easier to let iteration does its work.
}
}
/*
* Stores locale variables for comparison against Karney tables 4, 5 and 6. Values β₁ and β₂ are keep
* constant during all this method. Value α₁ is a first estimation and will be updated during iteration.
* Not that because Karney separate calculation of α₁ and remaining calculation in two separated tables,
* we need to truncate α₁ to the same number of digits than Karney in order to get the same numbers in
* the rest of this method.
*/
if (STORE_LOCAL_VARIABLES) {
store("β₁", atan(tanβ1));
store("β₂", atan(tanβ2));
store("α₁", α1);
α1 = computedToGiven1);
}
/*
* Refine α₁ using Newton's method. Karney proposes an hybrid approach: approximate α₁,
* then compute σ₁, σ₂, ω₁ and ω₂ (actually the sine and cosine of those angles in our
* implementation).
*/
double σ1, σ2;
int moreRefinements = Formulas.MAXIMUM_ITERATIONS;
do {
α0(msinα1 = sin1), mcosα1 = cos1), sinβ1, cosβ1);
final double k2 = computeSeriesExpansionCoefficients();
/*
* Compute α₂ from Karney equation 5: sin(α₀) = sin(α₁)⋅cos(β₁) = sin(α₂)⋅cos(β₂)
* The cos(α₂) term could be computed from sin(α₂), but Karnay recommends instead
* equation 45. An older publication (Karney 2010) went one step further with the
* replacement of (cos²β₂ - cos²β₁) by:
*
* (cosβ₂ - cosβ₁)⋅(cosβ₂ + cosβ₁) if β₁ < -π/4 (Reminder: β₁ is always negative)
* (sinβ₁ - sinβ₂)⋅(sinβ₁ + sinβ₂) otherwise.
*
* Actually we don't need α values directly, but instead cos(α)⋅cos(β).
* Note that cos(α₁) can be negative because α₁ ∈ [0…2π].
*/
final double cosα1_cosβ1 = mcosα1 * cosβ1;
final double cosα2_cosβ2 = sqrt(cosα1_cosβ1*cosα1_cosβ1 +
(cosβ1 <= 1/MathFunctions.SQRT_2 ? (cosβ2 - cosβ1)*(cosβ2 + cosβ1)
: (sinβ1 - sinβ2)*(sinβ1 + sinβ2)));
msinα2 = sinα0;
mcosα2 = cosα2_cosβ2;
/*
* Karney gives the following formulas:
*
* σ = atan2(sinβ, cosα⋅cosβ) — spherical arc length.
* ω = atan2(sin(α₀)⋅sinσ, cosσ) — spherical longitude.
*
* We perform the following substitutions where c is an unknown constant:
* That unknown coefficient will disaspear in atan2(c⋅y, c⋅x) expressions:
*
* sin(σ) = c⋅sinβ
* cos(σ) = c⋅cosα⋅cosβ
*/
final double ω1, ω2;
σ1 = atan2(sinβ1, cosα1_cosβ1); // (Karney 11)
σ2 = atan2(sinβ2, cosα2_cosβ2);
ω1 = atan2(sinβ1*sinα0, cosα1_cosβ1); // (Karney 12) with above-cited substitutions.
ω2 = atan2(sinβ2*sinα0, cosα2_cosβ2);
/*
* Compute the difference in longitude using the current start point and end point.
* If this difference is close enough to the requested accuracy, we are almost done.
* Otherwise refine the results. Note that if we detect that accuracy is good enough,
* we still complete the computation in order to not waste what we have computed so
* far in current iteration.
*/
final double λ2E;
λ1E = sphericalToGeodeticLongitude1, σ1);
λ2E = sphericalToGeodeticLongitude2, σ2);
final double Δλ_error = IEEEremainder2E - λ1E - Δλ, 2*PI);
if (abs(Δλ_error) <= ITERATION_TOLERANCE) {
moreRefinements = 0;
} else if (--moreRefinements == 0) {
throw new GeodeticException(Resources.format(Resources.Keys.NoConvergence));
}
/*
* Special case for α₁ = π/2 and β₂ = ±β₁ (Karney's equation 47). We replace the β₂ = ±β₁
* condition by |β₂| - |β₁| ≈ 0. Assuming tan(θ) ≈ θ for small angles we take the tangent
* of above difference and use tan(β₂ - β₁) = (tanβ₂ - tanβ₁)/(1 + tanβ₂⋅tanβ₁) identity.
* Note that tanβ₁ ≦ 0 and |tanβ₂| ≦ |tanβ₁| in this method.
*/
final double dΔλ_dα1;
if (abs(mcosα1) < LATITUDE_THRESHOLD && (-tanβ1 - abs(tanβ2)) < (1 + abs(tanβ1*tanβ2)) * LATITUDE_THRESHOLD) {
dΔλ_dα1 = -2 * sqrt(1 - eccentricitySquared * (cosβ1*cosβ1)) / sinβ1;
} else {
/*
* Karney's equation 38 combined with equation 46. The substitutions
* for sin(σ) and cos(σ) described above are applied again here.
*/
final double h1 = hypot(sinβ1, cosα1_cosβ1);
final double h2 = hypot(sinβ2, cosα2_cosβ2);
final double sinσ1 = sinβ1 / h1;
final double sinσ2 = sinβ2 / h2;
final double cosσ1 = cosα1_cosβ1 / h1;
final double cosσ2 = cosα2_cosβ2 / h2;
final double J2 = sphericalToEllipsoidalAngle2, true);
final double J1 = sphericalToEllipsoidalAngle1, true);
final double Δm = (sqrt(1 + k2*(sinσ2*sinσ2))*cosσ1*sinσ2
- sqrt(1 + k2*(sinσ1*sinσ1))*sinσ1*cosσ2
- cosσ1*cosσ2*(J2 - J1));
dΔλ_dα1 = Δm * axisRatio / cosα2_cosβ2;
if (STORE_LOCAL_VARIABLES) {
store("J(σ₁)", J1);
store("J(σ₂)", J2);
store("Δm", Δm * semiMinorAxis());
}
}
final double dα1 = Δλ_error / dΔλ_dα1; // Opposite sign of Karney δα₁ term.
α1 -= dα1;
if (STORE_LOCAL_VARIABLES) { // For comparing values against Karney table 5 and 6.
final double I1_σ2;
I1_σ1 = sphericalToEllipsoidalAngle1, false); // Required for computation of s₁ in `snapshot()`.
I1_σ2 = sphericalToEllipsoidalAngle2, false);
snapshot();
store("k²", k2);
store("σ₁", σ1);
store("σ₂", σ2);
store("ω₁", ω1);
store("ω₂", ω2);
store("α₂", atan2(msinα2, mcosα2));
store("λ₂", λ2E);
store("Δλ", λ2E - λ1E);
store("δλ", Δλ_error);
store("dλ/dα", dΔλ_dα1);
store("δσ₁", -dα1);
store("α₁", α1);
store("s₂", I1_σ2 * semiMinorAxis());
store("Δs", (I1_σ2 - I1_σ1) * semiMinorAxis());
}
} while (moreRefinements != 0);
final double I1_σ2;
I1_σ1 = sphericalToEllipsoidalAngle1, false);
I1_σ2 = sphericalToEllipsoidalAngle2, false);
geodesicDistance = (I1_σ2 - I1_σ1) * semiMinorAxis();
/*
* Restore the coordinate sign and order to the original configuration.
*/
if (swapPoints) {
double t;
t = msinα1; msinα1 = msinα2; msinα2 = t;
t = mcosα1; mcosα1 = mcosα2; mcosα2 = t;
}
if (inverseLongitudeSigns ^ swapPoints) {
msinα1 = -msinα1;
msinα2 = -msinα2;
}
if (inverseLatitudeSigns ^ swapPoints) {
mcosα1 = -mcosα1;
mcosα2 = -mcosα2;
}
setValid(STARTING_AZIMUTH | ENDING_AZIMUTH | GEODESIC_DISTANCE);
if (!(swapPoints | inverseLongitudeSigns | inverseLatitudeSigns)) {
setValid(COEFFICIENTS_FOR_START_POINT);
}
}
/**
* Computes the positive root of quartic equation for estimation of α₁ in nearly antipodal case.
* Formula is given in appendix B of <a href="https://arxiv.org/pdf/1102.1215.pdf">C.F.F Karney (2011)</a>
* given <var>x</var> and <var>y</var> the coordinates on a plane coordinate system centered on the antipodal point:
*
* {@preformat math
* μ⁴ + 2μ³ + (1−x²-y²)μ² − 2y²μ - y² = 0
* }
*
* The results should have only one positive root {@literal (μ > 0)}.
*
* <div class="section">Condition on <var>y</var> value</div>
* This method is indeterminate when <var>y</var> → 0 (it returns {@link Double#NaN}). For values too close to zero,
* the result may be non-significative because of rounding errors. For choosing a threshold value for <var>y</var>,
* {@code GeodesicsOnEllipsoidTest.Calculator} compares the value computed by this method against the value computed
* by {@link org.apache.sis.math.MathFunctions#polynomialRoots(double...)}. If the values differ too much, we presume
* that they are mostly noise caused by a <var>y</var> value too low.
*
* @param x2 the square of <var>x</var>.
* @param y2 the square of <var>y</var>.
*/
private static double μ(final double x2, final double y2) {
final double r = (x2 + y2 - 1)/6;
final double r3 = r*r*r;
final double S = x2*y2/4;
final double d = S*(S + 2*r3);
final double u;
if (d < 0) {
u = r*(1 + 2*cos(atan2(sqrt(-d), -(S + r3))/3));
} else {
final double T = cbrt(S + r3 + copySign(sqrt(d), S + r3));
u = (T == 0) ? 0 : r + T + r*r/T;
}
final double v = sqrt(u*u + y2);
final double w = (v + u - y2) / (2*v);
return (v + u) / (sqrt(v + u + w*w) + w);
}
/**
* Computes (∂y/∂φ)⁻¹ at the given latitude on an ellipsoid with semi-major axis length of 1.
* This derivative is close to cos(φ) for a slightly flattened sphere.
*
* @see org.apache.sis.referencing.operation.projection.ConformalProjection#dy_dφ
*/
@Override
double dφ_dy(final double φ) {
final double sinφ = sin(φ);
final double cosφ = cos(φ);
return cosφ/(1 - eccentricitySquared * (cosφ*cosφ) / (1 - eccentricitySquared * (sinφ*sinφ)));
}
/**
* Takes a snapshot of the current fields in this class. This is used for JUnit tests only. During development phases,
* {@link #STORE_LOCAL_VARIABLES} should be {@code true} for allowing {@code GeodesicsOnEllipsoidTest} to verify the
* values of a large range of local variables. But when the storage of locale variables is disabled, this method allows
* {@code GeodesicsOnEllipsoidTest} to still verify a few variables.
*/
final void snapshot() {
store("ε", ε);
store("A₁", A1);
store("A₂", A2);
store("A₃", A3);
store("α₀", atan2(sinα0, cosα0));
store("I₁(σ₁)", I1_σ1);
store("s₁", I1_σ1 * semiMinorAxis());
store("λ₁", λ1E);
}
/**
* Stores the value of a local variable or a field. This is used in JUnit tests only.
*
* @param name name of the local variable.
* @param value value of the local variable.
*/
void store(String name, double value) {
}
/**
* Replaces a computed value by the value given in Karney table. This is used when the result published in Karney
* table 4 is used as input for Karney table 5. We need to truncate the value to the same numbers of digits than
* Karney, otherwise computation results will differ.
*/
double computedToGiven(double α1) {
return α1;
}
/**
* Computes rhumb line using series expansion.
*
* <p><b>Source:</b> G.G. Bennett, 1996. <a href="https://doi.org/10.1017/S0373463300013151">
* Practical Rhumb Line Calculations on the Spheroid</a>. J. Navigation 49(1), 112-119.</p>
*/
@Override
final void computeRhumbLine() {
canComputeDistance();
/*
* Bennett (1996) equation 1 computes isometric latitudes Ψ for given geodetic latitudes φ:
*
* Ψ(φ) = log(tan(PI/4 + φ/2) * pow((1 - ℯsinφ) / (1 + ℯsinφ), ℯ/2));
*
* (ℯ is the eccentricity, not squared). However we need only the isometric latitudes difference:
*
* ΔΨ = Ψ(φ₂) - Ψ(φ₁)
*
* We rewrite the equation using log(Ψ₁) - log(Ψ₂) = log(Ψ₁/Ψ₂) and other identities:
*
* ΔΨ = log(tan(PI/4 + φ₂/2)) + log(pow((1 - ℯsinφ₂) / (1 + ℯsinφ₂), ℯ/2))
* - log(tan(PI/4 + φ₁/2)) - log(pow((1 - ℯsinφ₁) / (1 + ℯsinφ₁), ℯ/2))
*
* = log(tan(PI/4 + φ₂/2) /
* tan(PI/4 + φ₁/2) *
* pow(((1 - ℯsinφ₂)*(1 + ℯsinφ₁)) /
* ((1 + ℯsinφ₂)*(1 - ℯsinφ₁)), ℯ/2))
*
* The code below combines ℯsinφ terms otherwise. Note that we could also use product-to-sum
* identities for rewriting the tan(¼π + ½φ₂) / tan(¼π + ½φ₁) expression as (a + b) / (a - b)
* where a = cos((φ₂+φ₁)/2) and b = sin((φ₂-φ₁)/2), but the amount of trigonometric method calls
* would be about the same and result may be less accurate.
*/
final double eccentricity = sqrt(eccentricitySquared); // TODO: avoid computing on each invocation.
final double sinφ1 = sin1);
final double sinφ2 = sin2);
final double sd = eccentricity * (sinφ1 - sinφ2);
final double sm = 1 - eccentricitySquared * (sinφ1 * sinφ2);
final double ΔΨ = log(tan(PI/4 + φ2/2) / tan(PI/4 + φ1/2) * pow((sm+sd)/(sm-sd), eccentricity/2));
final double Δλ = IEEEremainder2 - λ1, 2*PI);
final double h = hypot(Δλ, ΔΨ);
final double S;
if (abs1 - φ2) < LATITUDE_THRESHOLD) {
final double φm = 1 + φ2)/2;
final double sinφ = sinm);
S = cosm) / sqrt(1 - eccentricitySquared*(sinφ*sinφ)); // Bennett equation 4 with sin(α) = Δλ/h.
} else {
final double m1 = m1, sinφ1);
final double m2 = m2, sinφ2);
S = (m2 - m1) / ΔΨ; // Bennett (1996) with cos(α) = ΔΨ/h.
if (STORE_LOCAL_VARIABLES) {
store("m₁", m1);
store("m₂", m2);
store("Δm", m2 - m1);
}
}
rhumblineLength = S * h * semiMajorAxis;
rhumblineAzimuth = atan2(Δλ, ΔΨ);
if (STORE_LOCAL_VARIABLES) {
store("Δλ", Δλ);
store("ΔΨ", ΔΨ);
}
}
/**
* Computes Bennett (1996) equation 2 modified with Clenshaw summation.
*/
private double m(final double φ, final double sinφ) {
final double cosφ = cos(φ);
final double sinθ = 2*sinφ*cosφ; // sin(2φ)
final double cosθ = (cosφ + sinφ) * (cosφ - sinφ); // cos(2φ) = cos²φ - sin²φ
return R0 + sinθ*(R2 + cosθ*(R4 + cosθ*R6));
}
}