1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
24pub enum GeodesicError {
25 #[error("invalid geodesic input {field}: {reason}")]
27 InvalidInput {
28 field: &'static str,
30 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 libm::hypot(value1, 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) = libm::sincos(reduced);
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 = libm::atan2(y, 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) = libm::sincos(sigma);
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 = libm::atan2(sbet1, 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) = libm::sincos(b11);
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) = libm::sincos(tau12);
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) = libm::sincos(sig12);
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 = libm::atan2(sbet2, (1.0 - WGS84_F) * cbet2);
411 let azi2_rad = libm::atan2(salp0, calp0 * csig2);
412 let somg1 = salp0 * ssig1;
413 let comg1 = csig1;
414 let somg2 = salp0 * ssig2;
415 let comg2 = csig2;
416 let omg12 = libm::atan2(somg2 * comg1 - comg2 * somg1, 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 = libm::atan2(
487 (csig1 * ssig2 - ssig1 * csig2).max(0.0),
488 csig1 * csig2 + ssig1 * ssig2,
489 );
490 let somg12 = (comg1 * somg2 - somg1 * comg2).max(0.0);
491 let comg12 = comg1 * comg2 + somg1 * somg2;
492 let k2 = WGS84_EP2 * sq(calp0);
493 let root = (1.0 + k2).sqrt();
494 let eps = k2 / (2.0 * (1.0 + root) + k2);
495 let series = series_from_salp0(salp0);
496 let b3 = sine_series_sin_cos(&series.c3, ssig2, csig2)
497 - sine_series_sin_cos(&series.c3, ssig1, csig1);
498 let eta = libm::atan2(
499 somg12 * target.clam - comg12 * target.slam,
500 comg12 * target.clam + somg12 * target.slam,
501 );
502 let residual_rad = eta - WGS84_F * salp0 * series.a3 * (sig12 + b3);
503 let sigma1 = SigmaPoint {
504 ssig: ssig1,
505 csig: csig1,
506 dn: point1.dn,
507 };
508 let sigma2 = SigmaPoint {
509 ssig: ssig2,
510 csig: csig2,
511 dn: point2.dn,
512 };
513 let lengths = inverse_lengths(eps, sig12, sigma1, sigma2);
514 let derivative_rad = if calp2 == 0.0 {
515 -2.0 * WGS84_ONE_MINUS_F * point1.dn / point1.sbet
516 } else {
517 lengths.reduced_length_over_b * WGS84_ONE_MINUS_F / (calp2 * point2.cbet)
518 };
519 let distance_m = WGS84_B_M * lengths.distance_over_b;
520 HybridSolution {
521 residual_rad,
522 distance_m,
523 salp1,
524 calp1,
525 salp2,
526 calp2,
527 derivative_rad,
528 }
529}
530
531fn hybrid_solution(
532 point1: ReducedPoint,
533 point2: ReducedPoint,
534 alpha1_rad: f64,
535 lambda12_rad: f64,
536) -> HybridSolution {
537 let (salp1, calp1) = libm::sincos(alpha1_rad);
538 let (slam12, clam12) = libm::sincos(lambda12_rad);
539 hybrid_solution_from_pair(
540 point1,
541 point2,
542 salp1,
543 calp1,
544 LongitudeTarget {
545 slam: slam12,
546 clam: clam12,
547 },
548 )
549}
550
551fn equatorial_inverse(lambda12_rad: f64) -> Option<HybridSolution> {
552 if lambda12_rad <= (1.0 - WGS84_F) * HALF_TURN_RAD {
553 Some(HybridSolution {
554 residual_rad: 0.0,
555 distance_m: WGS84_A_M * lambda12_rad,
556 salp1: 1.0,
557 calp1: 0.0,
558 salp2: 1.0,
559 calp2: 0.0,
560 derivative_rad: f64::NAN,
561 })
562 } else if (lambda12_rad - HALF_TURN_RAD).abs() <= f64::EPSILON {
563 let series = series_from_salp0(0.0);
564 Some(HybridSolution {
565 residual_rad: 0.0,
566 distance_m: WGS84_B_M * (i1(series, HALF_TURN_RAD) - i1(series, 0.0)),
567 salp1: 0.0,
568 calp1: 1.0,
569 salp2: 0.0,
570 calp2: -1.0,
571 derivative_rad: f64::NAN,
572 })
573 } else {
574 None
575 }
576}
577
578fn spherical_starting_azimuth(
579 point1: ReducedPoint,
580 point2: ReducedPoint,
581 lambda12_rad: f64,
582) -> (f64, f64) {
583 let cbetm = 0.5 * (point1.cbet + point2.cbet);
584 let wbar = (1.0 - WGS84_E2 * sq(cbetm)).sqrt();
585 let omega12 = lambda12_rad / wbar;
586 let (somg12, comg12) = libm::sincos(omega12);
587 let salp1 = point2.cbet * somg12;
588 let calp1 = point1.cbet * point2.sbet - point1.sbet * point2.cbet * comg12;
589 let (salp1, calp1) = normalize_pair(salp1, calp1);
590 if salp1 > 0.0 {
591 (salp1, calp1)
592 } else {
593 (TINY, calp1.signum())
594 }
595}
596
597fn astroid_mu(x: f64, y: f64) -> f64 {
598 let y2 = sq(y);
599 let mut low = 0.0;
600 let mut high = 1.0;
601 let eval = |mu: f64| sq(x / (1.0 + mu)) + y2 / sq(mu) - 1.0;
602
603 while eval(high) > 0.0 {
604 high *= 2.0;
605 }
606
607 for _ in 0..80 {
608 let mid = 0.5 * (low + high);
609 if eval(mid) > 0.0 {
610 low = mid;
611 } else {
612 high = mid;
613 }
614 }
615 0.5 * (low + high)
616}
617
618fn astroid_starting_azimuth(
619 point1: ReducedPoint,
620 point2: ReducedPoint,
621 lambda12_rad: f64,
622) -> Option<(f64, f64)> {
623 if lambda12_rad < HALF_TURN_RAD - 0.05 {
624 return None;
625 }
626
627 let scale_series = series_from_salp0(point1.cbet);
628 let lam_scale = WGS84_F * HALF_TURN_RAD * point1.cbet * scale_series.a3;
629 let bet_scale = lam_scale * point1.cbet;
630 if lam_scale <= 0.0 || bet_scale <= 0.0 {
631 return None;
632 }
633
634 let x = (lambda12_rad - HALF_TURN_RAD) / lam_scale;
635 let sbet_sum = point2.sbet * point1.cbet + point2.cbet * point1.sbet;
636 let cbet_sum = point1.cbet * point2.cbet - point1.sbet * point2.sbet;
637 let y = libm::atan2(sbet_sum, cbet_sum) / bet_scale;
638
639 if !(x <= 0.0 && y <= 0.0 && x >= -5.0 && y >= -5.0) {
640 return None;
641 }
642
643 let (salp1, calp1) = if y == 0.0 {
644 (-x, -(1.0 - sq(x)).max(0.0).sqrt())
645 } else {
646 let mu = astroid_mu(x, y);
647 (-x / (1.0 + mu), y / mu)
648 };
649 Some(normalize_pair(salp1, calp1))
650}
651
652fn starting_azimuth(
653 point1: ReducedPoint,
654 point2: ReducedPoint,
655 lambda12_rad: f64,
656) -> (f64, f64, bool) {
657 if let Some((salp1, calp1)) = astroid_starting_azimuth(point1, point2, lambda12_rad) {
658 (salp1, calp1, true)
659 } else {
660 let (salp1, calp1) = spherical_starting_azimuth(point1, point2, lambda12_rad);
661 (salp1, calp1, false)
662 }
663}
664
665fn canonical_inverse(phi1_deg: f64, phi2_deg: f64, lambda12_rad: f64) -> HybridSolution {
666 let point1 = reduced_point_deg(phi1_deg);
667 let point2 = reduced_point_deg(phi2_deg);
668
669 if lambda12_rad == 0.0 {
670 return hybrid_solution(point1, point2, 0.0, lambda12_rad);
671 }
672 if lambda12_rad == HALF_TURN_RAD {
673 return hybrid_solution(point1, point2, HALF_TURN_RAD, lambda12_rad);
674 }
675 if point1.sbet == 0.0 && point2.sbet == 0.0 {
676 if let Some(solution) = equatorial_inverse(lambda12_rad) {
677 return solution;
678 }
679 }
680 if point2.sbet == -point1.sbet && lambda12_rad > HALF_TURN_RAD - 0.05 {
681 let east_solution = hybrid_solution(point1, point2, 0.5 * HALF_TURN_RAD, lambda12_rad);
682 if east_solution.residual_rad.abs() <= 1.0e-12 {
683 return east_solution;
684 }
685 }
686 if let Some(solution) = short_inverse(point1, point2, lambda12_rad) {
687 return solution;
688 }
689
690 let (slam12, clam12) = libm::sincos(lambda12_rad);
691 let target = LongitudeTarget {
692 slam: slam12,
693 clam: clam12,
694 };
695 let (mut salp1, mut calp1, astroid_start) = starting_azimuth(point1, point2, lambda12_rad);
696 let mut salp1a = TINY;
697 let mut calp1a = 1.0;
698 let mut salp1b = TINY;
699 let mut calp1b = -1.0;
700 let mut candidate = hybrid_solution_from_pair(point1, point2, salp1, calp1, target);
701 let mut tripn = false;
702
703 for numit in 0..NEWTON_MAX {
704 let solution = hybrid_solution_from_pair(point1, point2, salp1, calp1, target);
705 candidate = solution;
706 let residual = solution.residual_rad;
707 let tolerance = if tripn {
708 8.0 * f64::EPSILON
709 } else {
710 f64::EPSILON
711 };
712 let alpha_correction = if astroid_start && solution.derivative_rad > 0.0 {
713 (-residual / solution.derivative_rad).abs()
714 } else {
715 0.0
716 };
717 if residual.abs() < tolerance && alpha_correction <= 8.0 * f64::EPSILON {
718 break;
719 }
720
721 if residual > 0.0 {
722 salp1b = salp1;
723 calp1b = calp1;
724 } else if residual < 0.0 {
725 salp1a = salp1;
726 calp1a = calp1;
727 }
728
729 if numit < NEWTON_FAST_MAX && solution.derivative_rad > 0.0 {
730 let dalp1 = -residual / solution.derivative_rad;
731 if dalp1.abs() < HALF_TURN_RAD {
732 let (sdalp1, cdalp1) = libm::sincos(dalp1);
733 let next_salp1 = salp1 * cdalp1 + calp1 * sdalp1;
734 if next_salp1 > 0.0 {
735 calp1 = calp1 * cdalp1 - salp1 * sdalp1;
736 salp1 = next_salp1;
737 (salp1, calp1) = normalize_pair(salp1, calp1);
738 tripn = residual.abs() <= 16.0 * f64::EPSILON;
739 continue;
740 }
741 }
742 }
743
744 let next_salp1 = 0.5 * (salp1a + salp1b);
745 let next_calp1 = 0.5 * (calp1a + calp1b);
746 if next_salp1 == salp1 && next_calp1 == calp1 {
747 break;
748 }
749 salp1 = next_salp1;
750 calp1 = next_calp1;
751 (salp1, calp1) = normalize_pair(salp1, calp1);
752 tripn = false;
753 }
754 candidate
755}
756
757fn short_line_threshold_rad() -> f64 {
758 0.1 * f64::EPSILON.sqrt()
759 / (0.5 * WGS84_F.abs().max(0.001) * (1.0 - WGS84_F / 2.0).min(1.0)).sqrt()
760}
761
762fn short_inverse(
763 point1: ReducedPoint,
764 point2: ReducedPoint,
765 lambda12_rad: f64,
766) -> Option<HybridSolution> {
767 let sbet12 = point2.sbet * point1.cbet - point2.cbet * point1.sbet;
768 let cbet12 = point2.cbet * point1.cbet + point2.sbet * point1.sbet;
769 if !(cbet12 >= 0.0 && sbet12 < 0.5 && point2.cbet * lambda12_rad < 0.5) {
770 return None;
771 }
772 let sbetm_sum = point1.sbet + point2.sbet;
773 let cbetm_sum = point1.cbet + point2.cbet;
774 let sbetm2 = sq(sbetm_sum) / (sq(sbetm_sum) + sq(cbetm_sum));
775 let dnm = (1.0 + WGS84_EP2 * sbetm2).sqrt();
776 let omega12 = lambda12_rad / (WGS84_ONE_MINUS_F * dnm);
777 let (somg12, comg12) = libm::sincos(omega12);
778 let sbet12a = point2.sbet * point1.cbet + point2.cbet * point1.sbet;
779 let mut salp1 = point2.cbet * somg12;
780 let mut calp1 = if comg12 >= 0.0 {
781 sbet12 + point2.cbet * point1.sbet * sq(somg12) / (1.0 + comg12)
782 } else {
783 sbet12a - point2.cbet * point1.sbet * sq(somg12) / (1.0 - comg12)
784 };
785 let ssig12 = hypot(salp1, calp1);
786 let csig12 = point1.sbet * point2.sbet + point1.cbet * point2.cbet * comg12;
787 if ssig12 >= short_line_threshold_rad() {
788 return None;
789 }
790 (salp1, calp1) = normalize_pair(salp1, calp1);
791 let mut salp2 = point1.cbet * somg12;
792 let mut calp2 = sbet12
793 - point1.cbet
794 * point2.sbet
795 * if comg12 >= 0.0 {
796 sq(somg12) / (1.0 + comg12)
797 } else {
798 1.0 - comg12
799 };
800 (salp2, calp2) = normalize_pair(salp2, calp2);
801 let sig12 = libm::atan2(ssig12, csig12);
802 Some(HybridSolution {
803 residual_rad: 0.0,
804 distance_m: sig12 * WGS84_B_M * dnm,
805 salp1,
806 calp1,
807 salp2,
808 calp2,
809 derivative_rad: f64::NAN,
810 })
811}
812
813fn transform_azimuths(
814 mut solution: HybridSolution,
815 swapped: bool,
816 lat_sign: f64,
817 lon_sign: f64,
818) -> (f64, f64) {
819 if swapped {
820 (solution.salp1, solution.salp2) = (-solution.salp2, -solution.salp1);
821 (solution.calp1, solution.calp2) = (-solution.calp2, -solution.calp1);
822 }
823 solution.calp1 *= lat_sign;
824 solution.calp2 *= lat_sign;
825 solution.salp1 *= lon_sign;
826 solution.salp2 *= lon_sign;
827 (
828 atan2_deg(solution.salp1, solution.calp1),
829 atan2_deg(solution.salp2, solution.calp2),
830 )
831}
832
833pub fn geodesic_inverse(
842 lat1_deg: f64,
843 lon1_deg: f64,
844 lat2_deg: f64,
845 lon2_deg: f64,
846) -> Result<(f64, f64, f64), GeodesicError> {
847 validate_latitude("lat1_deg", lat1_deg)?;
848 validate_finite("lon1_deg", lon1_deg)?;
849 validate_latitude("lat2_deg", lat2_deg)?;
850 validate_finite("lon2_deg", lon2_deg)?;
851
852 let mut phi1 = lat1_deg;
853 let mut phi2 = lat2_deg;
854 let lambda12 = longitude_delta_rad(lon1_deg, lon2_deg);
855 let mut lon_sign = if lambda12 < 0.0 { -1.0 } else { 1.0 };
856 let canonical_lambda12 = lambda12.abs();
857
858 let swapped = phi1.abs() < phi2.abs();
859 if swapped {
860 std::mem::swap(&mut phi1, &mut phi2);
861 lon_sign = -lon_sign;
862 }
863 let lat_sign = if phi1 > 0.0 { -1.0 } else { 1.0 };
864 phi1 *= lat_sign;
865 phi2 *= lat_sign;
866
867 let solution = canonical_inverse(phi1, phi2, canonical_lambda12);
868 let (azi1_deg, azi2_deg) = transform_azimuths(solution, swapped, lat_sign, lon_sign);
869 let distance_m = solution.distance_m.abs();
870 Ok((distance_m, azi1_deg, azi2_deg))
871}
872
873pub fn geodesic_direct(
880 lat1_deg: f64,
881 lon1_deg: f64,
882 azi1_deg: f64,
883 s12_m: f64,
884) -> Result<(f64, f64, f64), GeodesicError> {
885 validate_latitude("lat1_deg", lat1_deg)?;
886 validate_finite("lon1_deg", lon1_deg)?;
887 validate_finite("azi1_deg", azi1_deg)?;
888 validate_finite("s12_m", s12_m)?;
889
890 let (sbet1, cbet1) = reduced_lat_sin_cos_deg(lat1_deg);
891 let (salp1, calp1) = sin_cos_deg(azi1_deg);
892 let (lat2_rad, lambda12_rad, azi2_rad) = direct_raw(sbet1, cbet1, salp1, calp1, s12_m);
893 let lat2_deg = lat2_rad * RAD_TO_DEG;
894 let lon2_deg = normalize_lon_deg(lambda12_rad / DEG_TO_RAD + normalize_lon_deg(lon1_deg));
895 let azi2_deg = normalize_angle_deg(azi2_rad * RAD_TO_DEG);
896 Ok((lat2_deg, lon2_deg, azi2_deg))
897}