| /* |
| * 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 = ε2*ε2; |
| final double ε5 = ε2*ε3; |
| final double ε6 = ε3*ε3; |
| 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 = ε2*ε2; |
| final double ε5 = ε2*ε3; |
| final double ε6 = ε3*ε3; |
| 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 * tan(φ1); // β₁ 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 = sphericalToGeodeticLongitude(ω1, σ1); |
| I1_σ1 = sphericalToEllipsoidalAngle(σ1, 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 = sin(σ2); |
| final double cosσ2 = cos(σ2); |
| /* |
| * 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 = sphericalToGeodeticLongitude(ω2, σ2); |
| λ2 = IEEEremainder(λ2E - λ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 = this.φ1; |
| double φ2 = this.φ2; |
| double Δλ = IEEEremainder(λ2 - λ1, 2*PI); // In [-π … +π] range. |
| final boolean swapPoints = abs(φ2) > abs(φ1); |
| if (swapPoints) { |
| φ1 = this.φ2; |
| φ2 = this.φ1; |
| Δλ = -Δλ; |
| } |
| 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 * tan(φ1); |
| final double tanβ2 = axisRatio * tan(φ2); |
| 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_Δλ && abs(φ1 + φ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 = atan2(αy, α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(hypot(αy, α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 = computedToGiven(α1); |
| } |
| /* |
| * 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 = sin(α1), mcosα1 = cos(α1), 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 = sphericalToGeodeticLongitude(ω1, σ1); |
| λ2E = sphericalToGeodeticLongitude(ω2, σ2); |
| final double Δλ_error = IEEEremainder(λ2E - λ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 = sphericalToEllipsoidalAngle(σ2, true); |
| final double J1 = sphericalToEllipsoidalAngle(σ1, 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 = sphericalToEllipsoidalAngle(σ1, false); // Required for computation of s₁ in `snapshot()`. |
| I1_σ2 = sphericalToEllipsoidalAngle(σ2, 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 = sphericalToEllipsoidalAngle(σ1, false); |
| I1_σ2 = sphericalToEllipsoidalAngle(σ2, 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 = sin(φ1); |
| final double sinφ2 = sin(φ2); |
| 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 Δλ = IEEEremainder(λ2 - λ1, 2*PI); |
| final double h = hypot(Δλ, ΔΨ); |
| final double S; |
| if (abs(φ1 - φ2) < LATITUDE_THRESHOLD) { |
| final double φm = (φ1 + φ2)/2; |
| final double sinφ = sin(φm); |
| S = cos(φm) / sqrt(1 - eccentricitySquared*(sinφ*sinφ)); // Bennett equation 4 with sin(α) = Δλ/h. |
| } else { |
| final double m1 = m(φ1, sinφ1); |
| final double m2 = m(φ2, 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)); |
| } |
| } |