Skip to main content

sidereon_core/
geodesic.rs

1//! WGS84 geodesic direct and inverse helpers.
2//!
3//! The computations implement the order-6 series from Karney (2013),
4//! "Algorithms for geodesics", on the WGS84 ellipsoid. Public angles are
5//! degrees and distances are meters.
6
7use crate::constants::{DEG_TO_RAD, RAD_TO_DEG, WGS84_A_M, WGS84_F};
8
9const SERIES_ORDER: usize = 6;
10const HALF_TURN_RAD: f64 = std::f64::consts::PI;
11const HALF_TURN_DEG: f64 = 180.0;
12const TURN_DEG: f64 = 360.0;
13const TINY: f64 = 1.491_668_146_240_041_3e-154;
14const NEWTON_MAX: usize = 100;
15const NEWTON_FAST_MAX: usize = 20;
16const WGS84_B_M: f64 = WGS84_A_M * (1.0 - WGS84_F);
17const WGS84_ONE_MINUS_F: f64 = 1.0 - WGS84_F;
18const WGS84_E2: f64 = WGS84_F * (2.0 - WGS84_F);
19const WGS84_N: f64 = WGS84_F / (2.0 - WGS84_F);
20const WGS84_EP2: f64 = WGS84_F * (2.0 - WGS84_F) / ((1.0 - WGS84_F) * (1.0 - WGS84_F));
21
22/// Error returned when geodesic inputs are outside the accepted domain.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
24pub enum GeodesicError {
25    /// A geodesic input was non-finite or outside its documented range.
26    #[error("invalid geodesic input {field}: {reason}")]
27    InvalidInput {
28        /// Name of the invalid field.
29        field: &'static str,
30        /// Reason the field was rejected.
31        reason: &'static str,
32    },
33}
34
35#[derive(Debug, Clone, Copy)]
36struct Series {
37    a1: f64,
38    c1: [f64; SERIES_ORDER],
39    c1p: [f64; SERIES_ORDER],
40    a3: f64,
41    c3: [f64; SERIES_ORDER - 1],
42}
43
44#[derive(Debug, Clone, Copy)]
45struct HybridSolution {
46    residual_rad: f64,
47    distance_m: f64,
48    salp1: f64,
49    calp1: f64,
50    salp2: f64,
51    calp2: f64,
52    derivative_rad: f64,
53}
54
55#[derive(Debug, Clone, Copy)]
56struct Lengths {
57    distance_over_b: f64,
58    reduced_length_over_b: f64,
59}
60
61#[derive(Debug, Clone, Copy)]
62struct ReducedPoint {
63    sbet: f64,
64    cbet: f64,
65    dn: f64,
66}
67
68#[derive(Debug, Clone, Copy)]
69struct SigmaPoint {
70    ssig: f64,
71    csig: f64,
72    dn: f64,
73}
74
75#[derive(Debug, Clone, Copy)]
76struct LongitudeTarget {
77    slam: f64,
78    clam: f64,
79}
80
81fn invalid_input(field: &'static str, reason: &'static str) -> GeodesicError {
82    GeodesicError::InvalidInput { field, reason }
83}
84
85fn validate_latitude(field: &'static str, value: f64) -> Result<(), GeodesicError> {
86    if !value.is_finite() {
87        return Err(invalid_input(field, "must be finite"));
88    }
89    if !(-90.0..=90.0).contains(&value) {
90        return Err(invalid_input(field, "must be in [-90, 90] degrees"));
91    }
92    Ok(())
93}
94
95fn validate_finite(field: &'static str, value: f64) -> Result<(), GeodesicError> {
96    if value.is_finite() {
97        Ok(())
98    } else {
99        Err(invalid_input(field, "must be finite"))
100    }
101}
102
103fn sq(value: f64) -> f64 {
104    value * value
105}
106
107fn hypot(value1: f64, value2: f64) -> f64 {
108    value1.hypot(value2)
109}
110
111fn sin_cos_deg(value: f64) -> (f64, f64) {
112    let value = angle_round(value);
113    let mut reduced_deg = value % TURN_DEG;
114    let quadrant = (reduced_deg / 90.0).round();
115    reduced_deg -= 90.0 * quadrant;
116    let reduced = reduced_deg * DEG_TO_RAD;
117    let (sin_reduced, cos_reduced) = reduced.sin_cos();
118    let (sin_reduced, cos_reduced) = if reduced_deg.abs() == 45.0 {
119        let value = 0.5_f64.sqrt();
120        (value.copysign(reduced), value)
121    } else if reduced_deg.abs() == 30.0 {
122        (0.5_f64.copysign(reduced), 0.75_f64.sqrt())
123    } else {
124        (sin_reduced, cos_reduced)
125    };
126    match (quadrant as i64).rem_euclid(4) {
127        0 => (sin_reduced, cos_reduced),
128        1 => (cos_reduced, -sin_reduced),
129        2 => (-sin_reduced, -cos_reduced),
130        _ => (-cos_reduced, sin_reduced),
131    }
132}
133
134fn angle_round(value: f64) -> f64 {
135    let z = 1.0 / 16.0;
136    let y = value.abs();
137    let rounded = if y < z { z - (z - y) } else { y };
138    rounded.copysign(value)
139}
140
141fn normalize_pair(y: f64, x: f64) -> (f64, f64) {
142    let r = hypot(y, x);
143    if r == 0.0 {
144        (0.0, 1.0)
145    } else {
146        (y / r, x / r)
147    }
148}
149
150fn reduced_lat_sin_cos_deg(phi_deg: f64) -> (f64, f64) {
151    let (sin_phi, cos_phi) = sin_cos_deg(phi_deg);
152    let (sin_beta, cos_beta) = normalize_pair((1.0 - WGS84_F) * sin_phi, cos_phi);
153    (sin_beta, cos_beta.abs().max(TINY))
154}
155
156fn atan2_deg(y: f64, x: f64) -> f64 {
157    let mut y = y;
158    let mut x = x;
159    let mut quadrant = 0;
160    if y.abs() > x.abs() {
161        std::mem::swap(&mut y, &mut x);
162        quadrant = 2;
163    }
164    if x.is_sign_negative() {
165        x = -x;
166        quadrant += 1;
167    }
168    let mut angle = y.atan2(x) * RAD_TO_DEG;
169    match quadrant {
170        1 => angle = HALF_TURN_DEG.copysign(y) - angle,
171        2 => angle = 90.0 - angle,
172        3 => angle -= 90.0,
173        _ => {}
174    }
175    normalize_angle_deg(angle)
176}
177
178fn normalize_angle_deg(value: f64) -> f64 {
179    let normalized = (value + HALF_TURN_DEG).rem_euclid(TURN_DEG) - HALF_TURN_DEG;
180    if normalized == -HALF_TURN_DEG {
181        HALF_TURN_DEG
182    } else {
183        normalized
184    }
185}
186
187fn normalize_lon_deg(value: f64) -> f64 {
188    if value > -HALF_TURN_DEG && value <= HALF_TURN_DEG {
189        value
190    } else {
191        normalize_angle_deg(value)
192    }
193}
194
195fn longitude_delta_rad(lon1_deg: f64, lon2_deg: f64) -> f64 {
196    normalize_angle_deg(lon2_deg - lon1_deg) * DEG_TO_RAD
197}
198
199fn reduced_point_deg(phi_deg: f64) -> ReducedPoint {
200    let (sbet, cbet) = reduced_lat_sin_cos_deg(phi_deg);
201    ReducedPoint {
202        sbet,
203        cbet,
204        dn: (1.0 + WGS84_EP2 * sq(sbet)).sqrt(),
205    }
206}
207
208fn series_from_salp0(salp0: f64) -> Series {
209    let calp0 = (1.0 - sq(salp0)).max(0.0).sqrt();
210    let k2 = WGS84_EP2 * sq(calp0);
211    let root = (1.0 + k2).sqrt();
212    let eps = k2 / (2.0 * (1.0 + root) + k2);
213    let a1 = 1.0 + a1_minus1(eps);
214    let c1 = c1_coefficients(eps);
215    let c1p = c1p_coefficients(eps);
216    let a3x = a3_polynomial_coefficients();
217    let c3x = c3_polynomial_coefficients();
218    let a3 = polyval(a3x.len() - 1, &a3x, 0, eps);
219    let c3 = c3_coefficients(eps, &c3x);
220
221    Series {
222        a1,
223        c1,
224        c1p,
225        a3,
226        c3,
227    }
228}
229
230fn polyval(order: usize, coeffs: &[f64], offset: usize, x: f64) -> f64 {
231    let mut value = coeffs[offset];
232    for idx in 1..=order {
233        value = value * x + coeffs[offset + idx];
234    }
235    value
236}
237
238fn a1_minus1(eps: f64) -> f64 {
239    let eps2 = sq(eps);
240    let coeffs = [1.0, 4.0, 64.0, 0.0, 256.0];
241    let value = polyval(3, &coeffs, 0, eps2) / coeffs[4];
242    (value + eps) / (1.0 - eps)
243}
244
245fn c1_coefficients(eps: f64) -> [f64; SERIES_ORDER] {
246    let coeffs = [
247        -1.0, 6.0, -16.0, 32.0, -9.0, 64.0, -128.0, 2048.0, 9.0, -16.0, 768.0, 3.0, -5.0, 512.0,
248        -7.0, 1280.0, -7.0, 2048.0,
249    ];
250    coefficient_series(eps, &coeffs)
251}
252
253fn c1p_coefficients(eps: f64) -> [f64; SERIES_ORDER] {
254    let coeffs = [
255        205.0, -432.0, 768.0, 1536.0, 4005.0, -4736.0, 3840.0, 12288.0, -225.0, 116.0, 384.0,
256        -7173.0, 2695.0, 7680.0, 3467.0, 7680.0, 38081.0, 61440.0,
257    ];
258    coefficient_series(eps, &coeffs)
259}
260
261fn a2_minus1(eps: f64) -> f64 {
262    let eps2 = sq(eps);
263    let coeffs = [-11.0, -28.0, -192.0, 0.0, 256.0];
264    let value = polyval(3, &coeffs, 0, eps2) / coeffs[4];
265    (value - eps) / (1.0 + eps)
266}
267
268fn c2_coefficients(eps: f64) -> [f64; SERIES_ORDER] {
269    let coeffs = [
270        1.0, 2.0, 16.0, 32.0, 35.0, 64.0, 384.0, 2048.0, 15.0, 80.0, 768.0, 7.0, 35.0, 512.0, 63.0,
271        1280.0, 77.0, 2048.0,
272    ];
273    coefficient_series(eps, &coeffs)
274}
275
276fn coefficient_series(eps: f64, coeffs: &[f64]) -> [f64; SERIES_ORDER] {
277    let eps2 = sq(eps);
278    let mut result = [0.0; SERIES_ORDER];
279    let mut scale = eps;
280    let mut offset = 0;
281    for l in 1..=SERIES_ORDER {
282        let order = (SERIES_ORDER - l) / 2;
283        result[l - 1] = scale * polyval(order, coeffs, offset, eps2) / coeffs[offset + order + 1];
284        offset += order + 2;
285        scale *= eps;
286    }
287    result
288}
289
290fn a3_polynomial_coefficients() -> [f64; SERIES_ORDER] {
291    let coeffs = [
292        -3.0, 128.0, -2.0, -3.0, 64.0, -1.0, -3.0, -1.0, 16.0, 3.0, -1.0, -2.0, 8.0, 1.0, -1.0,
293        2.0, 1.0, 1.0,
294    ];
295    let mut result = [0.0; SERIES_ORDER];
296    let mut offset = 0;
297    for (out, j) in (0..SERIES_ORDER).rev().enumerate() {
298        let order = (SERIES_ORDER - j - 1).min(j);
299        result[out] = polyval(order, &coeffs, offset, WGS84_N) / coeffs[offset + order + 1];
300        offset += order + 2;
301    }
302    result
303}
304
305fn c3_polynomial_coefficients() -> [f64; SERIES_ORDER * (SERIES_ORDER - 1) / 2] {
306    let coeffs = [
307        3.0, 128.0, 2.0, 5.0, 128.0, -1.0, 3.0, 3.0, 64.0, -1.0, 0.0, 1.0, 8.0, -1.0, 1.0, 4.0,
308        5.0, 256.0, 1.0, 3.0, 128.0, -3.0, -2.0, 3.0, 64.0, 1.0, -3.0, 2.0, 32.0, 7.0, 512.0,
309        -10.0, 9.0, 384.0, 5.0, -9.0, 5.0, 192.0, 7.0, 512.0, -14.0, 7.0, 512.0, 21.0, 2560.0,
310    ];
311    let mut result = [0.0; SERIES_ORDER * (SERIES_ORDER - 1) / 2];
312    let mut offset = 0;
313    let mut out = 0;
314    for l in 1..SERIES_ORDER {
315        for j in (l..SERIES_ORDER).rev() {
316            let order = (SERIES_ORDER - j - 1).min(j);
317            result[out] = polyval(order, &coeffs, offset, WGS84_N) / coeffs[offset + order + 1];
318            out += 1;
319            offset += order + 2;
320        }
321    }
322    result
323}
324
325fn c3_coefficients(
326    eps: f64,
327    coeffs: &[f64; SERIES_ORDER * (SERIES_ORDER - 1) / 2],
328) -> [f64; SERIES_ORDER - 1] {
329    let mut result = [0.0; SERIES_ORDER - 1];
330    let mut scale = 1.0;
331    let mut offset = 0;
332    for l in 1..SERIES_ORDER {
333        let order = SERIES_ORDER - l - 1;
334        scale *= eps;
335        result[l - 1] = scale * polyval(order, coeffs, offset, eps);
336        offset += order + 1;
337    }
338    result
339}
340
341fn sine_series(coeffs: &[f64], sigma: f64) -> f64 {
342    let (sin_sigma, cos_sigma) = sigma.sin_cos();
343    sine_series_sin_cos(coeffs, sin_sigma, cos_sigma)
344}
345
346fn sine_series_sin_cos(coeffs: &[f64], sin_sigma: f64, cos_sigma: f64) -> f64 {
347    let two_cos = 2.0 * (cos_sigma - sin_sigma) * (cos_sigma + sin_sigma);
348    let mut k = coeffs.len() + 1;
349    let mut n = coeffs.len();
350    let mut y1 = 0.0;
351    let mut y0;
352    if n & 1 == 1 {
353        k -= 1;
354        y0 = coeffs[k - 1];
355    } else {
356        y0 = 0.0;
357    }
358    n /= 2;
359    while n > 0 {
360        n -= 1;
361        k -= 1;
362        y1 = two_cos * y0 - y1 + coeffs[k - 1];
363        k -= 1;
364        y0 = if k == 0 {
365            two_cos * y1 - y0
366        } else {
367            two_cos * y1 - y0 + coeffs[k - 1]
368        };
369    }
370    2.0 * sin_sigma * cos_sigma * y0
371}
372
373fn i1(series: Series, sigma: f64) -> f64 {
374    series.a1 * (sigma + sine_series(&series.c1, sigma))
375}
376
377fn sigma1_sin_cos(sbet1: f64, cbet1: f64, salp1: f64, calp1: f64) -> (f64, f64, f64) {
378    let (salp0, calp0) = normalize_pair(salp1 * cbet1, hypot(calp1, salp1 * sbet1));
379    let sigma1 = sbet1.atan2(calp1 * cbet1);
380    (sigma1, salp0, calp0)
381}
382
383fn direct_raw(sbet1: f64, cbet1: f64, salp1: f64, calp1: f64, s12_m: f64) -> (f64, f64, f64) {
384    let (_sigma1, salp0, calp0) = sigma1_sin_cos(sbet1, cbet1, salp1, calp1);
385    let series = series_from_salp0(salp0);
386    let (ssig1, csig1) = normalize_pair(
387        sbet1,
388        if sbet1 != 0.0 || calp1 != 0.0 {
389            cbet1 * calp1
390        } else {
391            1.0
392        },
393    );
394    let b11 = sine_series_sin_cos(&series.c1, ssig1, csig1);
395    let (sin_b11, cos_b11) = b11.sin_cos();
396    let stau1 = ssig1 * cos_b11 + csig1 * sin_b11;
397    let ctau1 = csig1 * cos_b11 - ssig1 * sin_b11;
398    let tau12 = s12_m / (WGS84_B_M * series.a1);
399    let (stau12, ctau12) = tau12.sin_cos();
400    let stau2 = stau1 * ctau12 + ctau1 * stau12;
401    let ctau2 = ctau1 * ctau12 - stau1 * stau12;
402    let b12 = -sine_series_sin_cos(&series.c1p, stau2, ctau2);
403    let sig12 = tau12 - (b12 - b11);
404    let (ssig12, csig12) = sig12.sin_cos();
405    let ssig2 = ssig1 * csig12 + csig1 * ssig12;
406    let csig2 = csig1 * csig12 - ssig1 * ssig12;
407
408    let sbet2 = calp0 * ssig2;
409    let cbet2 = hypot(salp0, calp0 * csig2);
410    let lat2_rad = sbet2.atan2((1.0 - WGS84_F) * cbet2);
411    let azi2_rad = salp0.atan2(calp0 * csig2);
412    let somg1 = salp0 * ssig1;
413    let comg1 = csig1;
414    let somg2 = salp0 * ssig2;
415    let comg2 = csig2;
416    let omg12 = (somg2 * comg1 - comg2 * somg1).atan2(comg2 * comg1 + somg2 * somg1);
417    let b31 = sine_series_sin_cos(&series.c3, ssig1, csig1);
418    let b32 = sine_series_sin_cos(&series.c3, ssig2, csig2);
419    let lambda12 = omg12 - WGS84_F * salp0 * series.a3 * (sig12 + b32 - b31);
420
421    (lat2_rad, lambda12, azi2_rad)
422}
423
424fn inverse_lengths(eps: f64, sig12: f64, point1: SigmaPoint, point2: SigmaPoint) -> Lengths {
425    let a1_minus1 = a1_minus1(eps);
426    let a1 = 1.0 + a1_minus1;
427    let c1 = c1_coefficients(eps);
428    let b1 = sine_series_sin_cos(&c1, point2.ssig, point2.csig)
429        - sine_series_sin_cos(&c1, point1.ssig, point1.csig);
430
431    let a2_minus1 = a2_minus1(eps);
432    let a2 = 1.0 + a2_minus1;
433    let c2 = c2_coefficients(eps);
434    let b2 = sine_series_sin_cos(&c2, point2.ssig, point2.csig)
435        - sine_series_sin_cos(&c2, point1.ssig, point1.csig);
436    let j12 = (a1_minus1 - a2_minus1) * sig12 + (a1 * b1 - a2 * b2);
437
438    Lengths {
439        distance_over_b: a1 * (sig12 + b1),
440        reduced_length_over_b: point2.dn * (point1.csig * point2.ssig)
441            - point1.dn * (point1.ssig * point2.csig)
442            - point1.csig * point2.csig * j12,
443    }
444}
445
446fn hybrid_solution_from_pair(
447    point1: ReducedPoint,
448    point2: ReducedPoint,
449    mut salp1: f64,
450    mut calp1: f64,
451    target: LongitudeTarget,
452) -> HybridSolution {
453    if point1.sbet == 0.0 && calp1 == 0.0 {
454        calp1 = -TINY;
455    }
456    (salp1, calp1) = normalize_pair(salp1, calp1);
457    let salp0 = salp1 * point1.cbet;
458    let calp0 = hypot(calp1, salp1 * point1.sbet);
459    let mut ssig1 = point1.sbet;
460    let mut csig1 = calp1 * point1.cbet;
461    let somg1 = salp0 * point1.sbet;
462    let comg1 = csig1;
463    (ssig1, csig1) = normalize_pair(ssig1, csig1);
464
465    let salp2 = if point2.cbet != point1.cbet {
466        salp0 / point2.cbet
467    } else {
468        salp1
469    };
470    let calp2 = if point2.cbet != point1.cbet || point2.sbet.abs() != -point1.sbet {
471        let sensitive = if point1.cbet < -point1.sbet {
472            (point2.cbet - point1.cbet) * (point1.cbet + point2.cbet)
473        } else {
474            (point1.sbet - point2.sbet) * (point1.sbet + point2.sbet)
475        };
476        (sq(calp1 * point1.cbet) + sensitive).max(0.0).sqrt() / point2.cbet
477    } else {
478        calp1.abs()
479    };
480    let mut ssig2 = point2.sbet;
481    let mut csig2 = calp2 * point2.cbet;
482    let somg2 = salp0 * point2.sbet;
483    let comg2 = csig2;
484    (ssig2, csig2) = normalize_pair(ssig2, csig2);
485
486    let sig12 = (csig1 * ssig2 - ssig1 * csig2)
487        .max(0.0)
488        .atan2(csig1 * csig2 + ssig1 * ssig2);
489    let somg12 = (comg1 * somg2 - somg1 * comg2).max(0.0);
490    let comg12 = comg1 * comg2 + somg1 * somg2;
491    let k2 = WGS84_EP2 * sq(calp0);
492    let root = (1.0 + k2).sqrt();
493    let eps = k2 / (2.0 * (1.0 + root) + k2);
494    let series = series_from_salp0(salp0);
495    let b3 = sine_series_sin_cos(&series.c3, ssig2, csig2)
496        - sine_series_sin_cos(&series.c3, ssig1, csig1);
497    let eta = (somg12 * target.clam - comg12 * target.slam)
498        .atan2(comg12 * target.clam + somg12 * target.slam);
499    let residual_rad = eta - WGS84_F * salp0 * series.a3 * (sig12 + b3);
500    let sigma1 = SigmaPoint {
501        ssig: ssig1,
502        csig: csig1,
503        dn: point1.dn,
504    };
505    let sigma2 = SigmaPoint {
506        ssig: ssig2,
507        csig: csig2,
508        dn: point2.dn,
509    };
510    let lengths = inverse_lengths(eps, sig12, sigma1, sigma2);
511    let derivative_rad = if calp2 == 0.0 {
512        -2.0 * WGS84_ONE_MINUS_F * point1.dn / point1.sbet
513    } else {
514        lengths.reduced_length_over_b * WGS84_ONE_MINUS_F / (calp2 * point2.cbet)
515    };
516    let distance_m = WGS84_B_M * lengths.distance_over_b;
517    HybridSolution {
518        residual_rad,
519        distance_m,
520        salp1,
521        calp1,
522        salp2,
523        calp2,
524        derivative_rad,
525    }
526}
527
528fn hybrid_solution(
529    point1: ReducedPoint,
530    point2: ReducedPoint,
531    alpha1_rad: f64,
532    lambda12_rad: f64,
533) -> HybridSolution {
534    let (salp1, calp1) = alpha1_rad.sin_cos();
535    let (slam12, clam12) = lambda12_rad.sin_cos();
536    hybrid_solution_from_pair(
537        point1,
538        point2,
539        salp1,
540        calp1,
541        LongitudeTarget {
542            slam: slam12,
543            clam: clam12,
544        },
545    )
546}
547
548fn equatorial_inverse(lambda12_rad: f64) -> Option<HybridSolution> {
549    if lambda12_rad <= (1.0 - WGS84_F) * HALF_TURN_RAD {
550        Some(HybridSolution {
551            residual_rad: 0.0,
552            distance_m: WGS84_A_M * lambda12_rad,
553            salp1: 1.0,
554            calp1: 0.0,
555            salp2: 1.0,
556            calp2: 0.0,
557            derivative_rad: f64::NAN,
558        })
559    } else if (lambda12_rad - HALF_TURN_RAD).abs() <= f64::EPSILON {
560        let series = series_from_salp0(0.0);
561        Some(HybridSolution {
562            residual_rad: 0.0,
563            distance_m: WGS84_B_M * (i1(series, HALF_TURN_RAD) - i1(series, 0.0)),
564            salp1: 0.0,
565            calp1: 1.0,
566            salp2: 0.0,
567            calp2: -1.0,
568            derivative_rad: f64::NAN,
569        })
570    } else {
571        None
572    }
573}
574
575fn spherical_starting_azimuth(
576    point1: ReducedPoint,
577    point2: ReducedPoint,
578    lambda12_rad: f64,
579) -> (f64, f64) {
580    let cbetm = 0.5 * (point1.cbet + point2.cbet);
581    let wbar = (1.0 - WGS84_E2 * sq(cbetm)).sqrt();
582    let omega12 = lambda12_rad / wbar;
583    let (somg12, comg12) = omega12.sin_cos();
584    let salp1 = point2.cbet * somg12;
585    let calp1 = point1.cbet * point2.sbet - point1.sbet * point2.cbet * comg12;
586    let (salp1, calp1) = normalize_pair(salp1, calp1);
587    if salp1 > 0.0 {
588        (salp1, calp1)
589    } else {
590        (TINY, calp1.signum())
591    }
592}
593
594fn astroid_mu(x: f64, y: f64) -> f64 {
595    let y2 = sq(y);
596    let mut low = 0.0;
597    let mut high = 1.0;
598    let eval = |mu: f64| sq(x / (1.0 + mu)) + y2 / sq(mu) - 1.0;
599
600    while eval(high) > 0.0 {
601        high *= 2.0;
602    }
603
604    for _ in 0..80 {
605        let mid = 0.5 * (low + high);
606        if eval(mid) > 0.0 {
607            low = mid;
608        } else {
609            high = mid;
610        }
611    }
612    0.5 * (low + high)
613}
614
615fn astroid_starting_azimuth(
616    point1: ReducedPoint,
617    point2: ReducedPoint,
618    lambda12_rad: f64,
619) -> Option<(f64, f64)> {
620    if lambda12_rad < HALF_TURN_RAD - 0.05 {
621        return None;
622    }
623
624    let scale_series = series_from_salp0(point1.cbet);
625    let lam_scale = WGS84_F * HALF_TURN_RAD * point1.cbet * scale_series.a3;
626    let bet_scale = lam_scale * point1.cbet;
627    if lam_scale <= 0.0 || bet_scale <= 0.0 {
628        return None;
629    }
630
631    let x = (lambda12_rad - HALF_TURN_RAD) / lam_scale;
632    let sbet_sum = point2.sbet * point1.cbet + point2.cbet * point1.sbet;
633    let cbet_sum = point1.cbet * point2.cbet - point1.sbet * point2.sbet;
634    let y = sbet_sum.atan2(cbet_sum) / bet_scale;
635
636    if !(x <= 0.0 && y <= 0.0 && x >= -5.0 && y >= -5.0) {
637        return None;
638    }
639
640    let (salp1, calp1) = if y == 0.0 {
641        (-x, -(1.0 - sq(x)).max(0.0).sqrt())
642    } else {
643        let mu = astroid_mu(x, y);
644        (-x / (1.0 + mu), y / mu)
645    };
646    Some(normalize_pair(salp1, calp1))
647}
648
649fn starting_azimuth(
650    point1: ReducedPoint,
651    point2: ReducedPoint,
652    lambda12_rad: f64,
653) -> (f64, f64, bool) {
654    if let Some((salp1, calp1)) = astroid_starting_azimuth(point1, point2, lambda12_rad) {
655        (salp1, calp1, true)
656    } else {
657        let (salp1, calp1) = spherical_starting_azimuth(point1, point2, lambda12_rad);
658        (salp1, calp1, false)
659    }
660}
661
662fn canonical_inverse(phi1_deg: f64, phi2_deg: f64, lambda12_rad: f64) -> HybridSolution {
663    let point1 = reduced_point_deg(phi1_deg);
664    let point2 = reduced_point_deg(phi2_deg);
665
666    if lambda12_rad == 0.0 {
667        return hybrid_solution(point1, point2, 0.0, lambda12_rad);
668    }
669    if lambda12_rad == HALF_TURN_RAD {
670        return hybrid_solution(point1, point2, HALF_TURN_RAD, lambda12_rad);
671    }
672    if point1.sbet == 0.0 && point2.sbet == 0.0 {
673        if let Some(solution) = equatorial_inverse(lambda12_rad) {
674            return solution;
675        }
676    }
677    if point2.sbet == -point1.sbet && lambda12_rad > HALF_TURN_RAD - 0.05 {
678        let east_solution = hybrid_solution(point1, point2, 0.5 * HALF_TURN_RAD, lambda12_rad);
679        if east_solution.residual_rad.abs() <= 1.0e-12 {
680            return east_solution;
681        }
682    }
683    if let Some(solution) = short_inverse(point1, point2, lambda12_rad) {
684        return solution;
685    }
686
687    let (slam12, clam12) = lambda12_rad.sin_cos();
688    let target = LongitudeTarget {
689        slam: slam12,
690        clam: clam12,
691    };
692    let (mut salp1, mut calp1, astroid_start) = starting_azimuth(point1, point2, lambda12_rad);
693    let mut salp1a = TINY;
694    let mut calp1a = 1.0;
695    let mut salp1b = TINY;
696    let mut calp1b = -1.0;
697    let mut candidate = hybrid_solution_from_pair(point1, point2, salp1, calp1, target);
698    let mut tripn = false;
699
700    for numit in 0..NEWTON_MAX {
701        let solution = hybrid_solution_from_pair(point1, point2, salp1, calp1, target);
702        candidate = solution;
703        let residual = solution.residual_rad;
704        let tolerance = if tripn {
705            8.0 * f64::EPSILON
706        } else {
707            f64::EPSILON
708        };
709        let alpha_correction = if astroid_start && solution.derivative_rad > 0.0 {
710            (-residual / solution.derivative_rad).abs()
711        } else {
712            0.0
713        };
714        if residual.abs() < tolerance && alpha_correction <= 8.0 * f64::EPSILON {
715            break;
716        }
717
718        if residual > 0.0 {
719            salp1b = salp1;
720            calp1b = calp1;
721        } else if residual < 0.0 {
722            salp1a = salp1;
723            calp1a = calp1;
724        }
725
726        if numit < NEWTON_FAST_MAX && solution.derivative_rad > 0.0 {
727            let dalp1 = -residual / solution.derivative_rad;
728            if dalp1.abs() < HALF_TURN_RAD {
729                let (sdalp1, cdalp1) = dalp1.sin_cos();
730                let next_salp1 = salp1 * cdalp1 + calp1 * sdalp1;
731                if next_salp1 > 0.0 {
732                    calp1 = calp1 * cdalp1 - salp1 * sdalp1;
733                    salp1 = next_salp1;
734                    (salp1, calp1) = normalize_pair(salp1, calp1);
735                    tripn = residual.abs() <= 16.0 * f64::EPSILON;
736                    continue;
737                }
738            }
739        }
740
741        let next_salp1 = 0.5 * (salp1a + salp1b);
742        let next_calp1 = 0.5 * (calp1a + calp1b);
743        if next_salp1 == salp1 && next_calp1 == calp1 {
744            break;
745        }
746        salp1 = next_salp1;
747        calp1 = next_calp1;
748        (salp1, calp1) = normalize_pair(salp1, calp1);
749        tripn = false;
750    }
751    candidate
752}
753
754fn short_line_threshold_rad() -> f64 {
755    0.1 * f64::EPSILON.sqrt()
756        / (0.5 * WGS84_F.abs().max(0.001) * (1.0 - WGS84_F / 2.0).min(1.0)).sqrt()
757}
758
759fn short_inverse(
760    point1: ReducedPoint,
761    point2: ReducedPoint,
762    lambda12_rad: f64,
763) -> Option<HybridSolution> {
764    let sbet12 = point2.sbet * point1.cbet - point2.cbet * point1.sbet;
765    let cbet12 = point2.cbet * point1.cbet + point2.sbet * point1.sbet;
766    if !(cbet12 >= 0.0 && sbet12 < 0.5 && point2.cbet * lambda12_rad < 0.5) {
767        return None;
768    }
769    let sbetm_sum = point1.sbet + point2.sbet;
770    let cbetm_sum = point1.cbet + point2.cbet;
771    let sbetm2 = sq(sbetm_sum) / (sq(sbetm_sum) + sq(cbetm_sum));
772    let dnm = (1.0 + WGS84_EP2 * sbetm2).sqrt();
773    let omega12 = lambda12_rad / (WGS84_ONE_MINUS_F * dnm);
774    let (somg12, comg12) = omega12.sin_cos();
775    let sbet12a = point2.sbet * point1.cbet + point2.cbet * point1.sbet;
776    let mut salp1 = point2.cbet * somg12;
777    let mut calp1 = if comg12 >= 0.0 {
778        sbet12 + point2.cbet * point1.sbet * sq(somg12) / (1.0 + comg12)
779    } else {
780        sbet12a - point2.cbet * point1.sbet * sq(somg12) / (1.0 - comg12)
781    };
782    let ssig12 = hypot(salp1, calp1);
783    let csig12 = point1.sbet * point2.sbet + point1.cbet * point2.cbet * comg12;
784    if ssig12 >= short_line_threshold_rad() {
785        return None;
786    }
787    (salp1, calp1) = normalize_pair(salp1, calp1);
788    let mut salp2 = point1.cbet * somg12;
789    let mut calp2 = sbet12
790        - point1.cbet
791            * point2.sbet
792            * if comg12 >= 0.0 {
793                sq(somg12) / (1.0 + comg12)
794            } else {
795                1.0 - comg12
796            };
797    (salp2, calp2) = normalize_pair(salp2, calp2);
798    let sig12 = ssig12.atan2(csig12);
799    Some(HybridSolution {
800        residual_rad: 0.0,
801        distance_m: sig12 * WGS84_B_M * dnm,
802        salp1,
803        calp1,
804        salp2,
805        calp2,
806        derivative_rad: f64::NAN,
807    })
808}
809
810fn transform_azimuths(
811    mut solution: HybridSolution,
812    swapped: bool,
813    lat_sign: f64,
814    lon_sign: f64,
815) -> (f64, f64) {
816    if swapped {
817        (solution.salp1, solution.salp2) = (-solution.salp2, -solution.salp1);
818        (solution.calp1, solution.calp2) = (-solution.calp2, -solution.calp1);
819    }
820    solution.calp1 *= lat_sign;
821    solution.calp2 *= lat_sign;
822    solution.salp1 *= lon_sign;
823    solution.salp2 *= lon_sign;
824    (
825        atan2_deg(solution.salp1, solution.calp1),
826        atan2_deg(solution.salp2, solution.calp2),
827    )
828}
829
830/// Solve the WGS84 inverse geodesic problem.
831///
832/// Inputs are point 1 latitude and longitude followed by point 2 latitude and
833/// longitude, all in degrees. Longitudes may be outside `[-180, 180]`; they are
834/// reduced by the geodesic solver. The returned tuple is `(s12_m, azi1_deg,
835/// azi2_deg)`, where `s12_m` is the geodesic distance in meters, `azi1_deg` is
836/// the forward azimuth at point 1, and `azi2_deg` is the forward azimuth at
837/// point 2.
838pub fn geodesic_inverse(
839    lat1_deg: f64,
840    lon1_deg: f64,
841    lat2_deg: f64,
842    lon2_deg: f64,
843) -> Result<(f64, f64, f64), GeodesicError> {
844    validate_latitude("lat1_deg", lat1_deg)?;
845    validate_finite("lon1_deg", lon1_deg)?;
846    validate_latitude("lat2_deg", lat2_deg)?;
847    validate_finite("lon2_deg", lon2_deg)?;
848
849    let mut phi1 = lat1_deg;
850    let mut phi2 = lat2_deg;
851    let lambda12 = longitude_delta_rad(lon1_deg, lon2_deg);
852    let mut lon_sign = if lambda12 < 0.0 { -1.0 } else { 1.0 };
853    let canonical_lambda12 = lambda12.abs();
854
855    let swapped = phi1.abs() < phi2.abs();
856    if swapped {
857        std::mem::swap(&mut phi1, &mut phi2);
858        lon_sign = -lon_sign;
859    }
860    let lat_sign = if phi1 > 0.0 { -1.0 } else { 1.0 };
861    phi1 *= lat_sign;
862    phi2 *= lat_sign;
863
864    let solution = canonical_inverse(phi1, phi2, canonical_lambda12);
865    let (azi1_deg, azi2_deg) = transform_azimuths(solution, swapped, lat_sign, lon_sign);
866    let distance_m = solution.distance_m.abs();
867    Ok((distance_m, azi1_deg, azi2_deg))
868}
869
870/// Solve the WGS84 direct geodesic problem.
871///
872/// Inputs are point 1 latitude, longitude, forward azimuth, and geodesic
873/// distance. Angles are degrees and `s12_m` is meters. Longitudes and azimuths
874/// may be outside one turn; they are reduced by the geodesic solver. The
875/// returned tuple is `(lat2_deg, lon2_deg, azi2_deg)`.
876pub fn geodesic_direct(
877    lat1_deg: f64,
878    lon1_deg: f64,
879    azi1_deg: f64,
880    s12_m: f64,
881) -> Result<(f64, f64, f64), GeodesicError> {
882    validate_latitude("lat1_deg", lat1_deg)?;
883    validate_finite("lon1_deg", lon1_deg)?;
884    validate_finite("azi1_deg", azi1_deg)?;
885    validate_finite("s12_m", s12_m)?;
886
887    let (sbet1, cbet1) = reduced_lat_sin_cos_deg(lat1_deg);
888    let (salp1, calp1) = sin_cos_deg(azi1_deg);
889    let (lat2_rad, lambda12_rad, azi2_rad) = direct_raw(sbet1, cbet1, salp1, calp1, s12_m);
890    let lat2_deg = lat2_rad * RAD_TO_DEG;
891    let lon2_deg = normalize_lon_deg(lambda12_rad / DEG_TO_RAD + normalize_lon_deg(lon1_deg));
892    let azi2_deg = normalize_angle_deg(azi2_rad * RAD_TO_DEG);
893    Ok((lat2_deg, lon2_deg, azi2_deg))
894}