UNPKG

igc-xc-score

Version:

igc-xc-score is a paragliding and hang-gliding XC scoring program in vanilla JS

79 lines (66 loc) 3.94 kB
'use strict'; import * as util from './util.js'; // Vincenty's Algorithm, courtesy of Movable Type Ltd // https://www.movable-type.co.uk/scripts/latlong-vincenty.html // Published and included here under an MIT licence export function inverse(p1, p2) { const φ1 = util.radians(p1.y), λ1 = util.radians(p1.x); const φ2 = util.radians(p2.y), λ2 = util.radians(p2.x); const { a, b, f } = util.WGS84; const L = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ. const tanU1 = (1 - f) * Math.tan1); const cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1)); const sinU1 = tanU1 * cosU1; const tanU2 = (1 - f) * Math.tan2); const cosU2 = 1 / Math.sqrt((1 + tanU2 * tanU2)); const sinU2 = tanU2 * cosU2; const sinU1sinU2 = sinU1 * sinU2; const cosU1cosU2 = cosU1 * cosU2; const cosU1sinU2 = cosU1 * sinU2; const sinU1cosU2 = sinU1 * cosU2; const antipodal = Math.abs(L) > Math.PI / 2 || Math.abs2 - φ1) > Math.PI / 2; let λ = L, sinλ = null, cosλ = null; // λ = difference in longitude on an auxiliary sphere let σ = antipodal ? Math.PI : 0, sinσ = 0, cosσ = antipodal ? -1 : 1, sinSqσ = null; // σ = angular distance P₁ P₂ on the sphere let cos2σₘ = 1; // σₘ = angular distance on the sphere from the equator to the midpoint of the line let sinα = null, cosSqα = 1; // α = azimuth of the geodesic at the equator let C = null; let λʹ = null, iterations = 0; do { sinλ = Math.sin(λ); cosλ = Math.cos(λ); const term1 = cosU2 * sinλ; const term2 = cosU1sinU2 - sinU1cosU2 * cosλ; sinSqσ = term1 * term1 + term2 * term2; if (Math.abs(sinSqσ) < Number.EPSILON) break; // co-incident/antipodal points (falls back on λ/σ = L) sinσ = Math.sqrt(sinSqσ); cosσ = sinU1sinU2 + cosU1cosU2 * cosλ; σ = Math.atan2(sinσ, cosσ); sinα = cosU1cosU2 * sinλ / sinσ; cosSqα = 1 - sinα * sinα; cos2σₘ = (cosSqα != 0) ? (cosσ - 2 * sinU1sinU2 / cosSqα) : 0; // on equatorial line cos²α = 0 (§6) C = f / 16 * cosSqα * (4 + f * (4 - 3 * cosSqα)); λʹ = λ; λ = L + (1 - C) * f * sinα * (σ + C * sinσ * (cos2σₘ + C * cosσ * (-1 + 2 * cos2σₘ * cos2σₘ))); const iterationCheck = antipodal ? Math.abs(λ) - Math.PI : Math.abs(λ); if (iterationCheck > Math.PI) throw new EvalError('λ > π'); } while (Math.abs(λ - λʹ) > 1e-7 && ++iterations < 1000); if (iterations >= 1000) throw new EvalError('Vincenty formula failed to converge'); const uSq = cosSqα * (a * a - b * b) / (b * b); const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))); const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); const Δσ = B * sinσ * (cos2σₘ + B / 4 * (cosσ * (-1 + 2 * cos2σₘ * cos2σₘ) - B / 6 * cos2σₘ * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σₘ * cos2σₘ))); const s = b * A * (σ - Δσ); // s = length of the geodesic // note special handling of exactly antipodal points where sin²σ = 0 (due to discontinuity // atan2(0, 0) = 0 but atan2(ε, 0) = π/2 / 90°) - in which case bearing is always meridional, // due north (or due south!) // α = azimuths of the geodesic; α2 the direction P₁ P₂ produced const α1 = Math.abs(sinSqσ) < Number.EPSILON ? 0 : Math.atan2(cosU2 * sinλ, cosU1sinU2 - sinU1cosU2 * cosλ); const α2 = Math.abs(sinSqσ) < Number.EPSILON ? Math.PI : Math.atan2(cosU1 * sinλ, -sinU1cosU2 + cosU1sinU2 * cosλ); return { distance: s, initialBearing: Math.abs(s) < Number.EPSILON ? NaN : util.degrees1), finalBearing: Math.abs(s) < Number.EPSILON ? NaN : util.degrees2), iterations: iterations, }; }