1#[cfg(not(feature = "std"))]
26use alloc::string::{String, ToString};
27
28use core::fmt;
29
30pub const WGS84_A: f64 = 6_378_137.0;
36
37pub const WGS84_B: f64 = 6_356_752.314_245_179;
39
40pub const WGS84_MEAN_RADIUS: f64 = 6_371_008.8;
43
44pub const DEFAULT_MAX_ITER: u32 = 100;
46
47pub const DEFAULT_TOL: f64 = 1e-12;
49
50#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct GeodesicParams {
60 pub a: f64,
62 pub b: f64,
64 pub max_iter: u32,
66 pub tol: f64,
68}
69
70impl GeodesicParams {
71 pub fn new(a: f64, b: f64) -> Self {
73 Self {
74 a,
75 b,
76 max_iter: DEFAULT_MAX_ITER,
77 tol: DEFAULT_TOL,
78 }
79 }
80
81 pub fn with_convergence(a: f64, b: f64, max_iter: u32, tol: f64) -> Self {
83 Self {
84 a,
85 b,
86 max_iter,
87 tol,
88 }
89 }
90
91 pub fn wgs84() -> Self {
93 Self::new(WGS84_A, WGS84_B)
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct VincentyResult {
104 pub distance_m: f64,
106 pub azimuth_fwd_deg: f64,
108 pub azimuth_rev_deg: f64,
110 pub iterations: u32,
112}
113
114impl fmt::Display for VincentyResult {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(
117 f,
118 "distance={:.3} m, fwd_az={:.6}°, rev_az={:.6}°, iters={}",
119 self.distance_m, self.azimuth_fwd_deg, self.azimuth_rev_deg, self.iterations
120 )
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
126pub struct VincentyDirectResult {
127 pub lat2_deg: f64,
129 pub lon2_deg: f64,
131 pub azimuth_rev_deg: f64,
133}
134
135impl fmt::Display for VincentyDirectResult {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(
138 f,
139 "lat={:.8}°, lon={:.8}°, rev_az={:.6}°",
140 self.lat2_deg, self.lon2_deg, self.azimuth_rev_deg
141 )
142 }
143}
144
145#[derive(Debug, Clone, PartialEq)]
151pub enum GeodesicError {
152 AntipodalPoints,
154 InvalidInput(String),
156}
157
158impl fmt::Display for GeodesicError {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 match self {
161 GeodesicError::AntipodalPoints => write!(
162 f,
163 "Vincenty inverse failed to converge: points are nearly antipodal"
164 ),
165 GeodesicError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
166 }
167 }
168}
169
170#[cfg(feature = "std")]
171impl std::error::Error for GeodesicError {}
172
173#[inline]
179fn azi_to_deg(azi_rad: f64) -> f64 {
180 let deg = azi_rad.to_degrees();
181 (deg % 360.0 + 360.0) % 360.0
182}
183
184fn validate_lat_lon(lat: f64, lon: f64, label: &str) -> Result<(), GeodesicError> {
186 if !lat.is_finite() || !lon.is_finite() {
187 return Err(GeodesicError::InvalidInput(
188 #[cfg(feature = "std")]
189 format!("{label}: lat/lon must be finite numbers (got lat={lat}, lon={lon})"),
190 #[cfg(not(feature = "std"))]
191 "lat/lon must be finite numbers".to_string(),
192 ));
193 }
194 if !(-90.0..=90.0).contains(&lat) {
195 return Err(GeodesicError::InvalidInput(
196 #[cfg(feature = "std")]
197 format!("{label}: latitude {lat} is outside [-90, 90]"),
198 #[cfg(not(feature = "std"))]
199 "latitude out of range [-90, 90]".to_string(),
200 ));
201 }
202 if !(-180.0..=180.0).contains(&lon) {
203 return Err(GeodesicError::InvalidInput(
204 #[cfg(feature = "std")]
205 format!("{label}: longitude {lon} is outside [-180, 180]"),
206 #[cfg(not(feature = "std"))]
207 "longitude out of range [-180, 180]".to_string(),
208 ));
209 }
210 Ok(())
211}
212
213pub fn vincenty_inverse(
228 lat1_deg: f64,
229 lon1_deg: f64,
230 lat2_deg: f64,
231 lon2_deg: f64,
232 params: GeodesicParams,
233) -> Result<VincentyResult, GeodesicError> {
234 validate_lat_lon(lat1_deg, lon1_deg, "point 1")?;
235 validate_lat_lon(lat2_deg, lon2_deg, "point 2")?;
236 let GeodesicParams {
237 a,
238 b,
239 max_iter,
240 tol,
241 } = params;
242
243 if (lat1_deg - lat2_deg).abs() < f64::EPSILON && (lon1_deg - lon2_deg).abs() < f64::EPSILON {
245 return Ok(VincentyResult {
246 distance_m: 0.0,
247 azimuth_fwd_deg: 0.0,
248 azimuth_rev_deg: 0.0,
249 iterations: 0,
250 });
251 }
252
253 let f = (a - b) / a; let phi1 = lat1_deg.to_radians();
257 let phi2 = lat2_deg.to_radians();
258 let l = (lon2_deg - lon1_deg).to_radians();
259
260 let one_minus_f = 1.0 - f;
262 let u1 = (one_minus_f * phi1.tan()).atan();
263 let u2 = (one_minus_f * phi2.tan()).atan();
264
265 let sin_u1 = u1.sin();
266 let cos_u1 = u1.cos();
267 let sin_u2 = u2.sin();
268 let cos_u2 = u2.cos();
269
270 struct IterState {
272 sin_sigma: f64,
273 cos_sigma: f64,
274 sigma: f64,
275 cos2_alpha: f64,
276 cos2_sigma_m: f64,
277 lambda: f64,
278 }
279
280 #[inline]
282 fn vincenty_iter_step(
283 lambda: f64,
284 l: f64,
285 f: f64,
286 sin_u1: f64,
287 cos_u1: f64,
288 sin_u2: f64,
289 cos_u2: f64,
290 ) -> IterState {
291 let sin_lambda = lambda.sin();
292 let cos_lambda = lambda.cos();
293
294 let term_a = cos_u2 * sin_lambda;
295 let term_b = cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda;
296 let sin_sigma = (term_a * term_a + term_b * term_b).sqrt();
297 let cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
298 let sigma = sin_sigma.atan2(cos_sigma);
299
300 let sin_alpha = if sin_sigma.abs() < f64::EPSILON {
301 0.0
302 } else {
303 cos_u1 * cos_u2 * sin_lambda / sin_sigma
304 };
305 let cos2_alpha = 1.0 - sin_alpha * sin_alpha;
306
307 let cos2_sigma_m = if cos2_alpha.abs() < f64::EPSILON {
308 0.0
309 } else {
310 cos_sigma - 2.0 * sin_u1 * sin_u2 / cos2_alpha
311 };
312
313 let c = f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
314 let new_lambda = l
315 + (1.0 - c)
316 * f
317 * sin_alpha
318 * (sigma
319 + c * sin_sigma
320 * (cos2_sigma_m
321 + c * cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)));
322
323 IterState {
324 sin_sigma,
325 cos_sigma,
326 sigma,
327 cos2_alpha,
328 cos2_sigma_m,
329 lambda: new_lambda,
330 }
331 }
332
333 let mut lambda = l;
335 let mut iter: u32 = 0;
336 let state = loop {
337 iter += 1;
338 let state = vincenty_iter_step(lambda, l, f, sin_u1, cos_u1, sin_u2, cos_u2);
339 let delta = (state.lambda - lambda).abs();
340 lambda = state.lambda;
341
342 if delta < tol {
343 break state;
344 }
345
346 if iter >= max_iter {
347 return Err(GeodesicError::AntipodalPoints);
348 }
349 };
350
351 let sin_sigma = state.sin_sigma;
352 let cos_sigma = state.cos_sigma;
353 let sigma = state.sigma;
354 let cos2_alpha = state.cos2_alpha;
355 let cos2_sigma_m = state.cos2_sigma_m;
356 lambda = state.lambda;
357
358 let u2_sq = cos2_alpha * (a * a - b * b) / (b * b);
360
361 let big_a =
362 1.0 + u2_sq / 16384.0 * (4096.0 + u2_sq * (-768.0 + u2_sq * (320.0 - 175.0 * u2_sq)));
363 let big_b = u2_sq / 1024.0 * (256.0 + u2_sq * (-128.0 + u2_sq * (74.0 - 47.0 * u2_sq)));
364
365 let cos2_sigma_m_sq = cos2_sigma_m * cos2_sigma_m;
366 let sin_sigma_sq = sin_sigma * sin_sigma;
367
368 let delta_sigma = big_b
369 * sin_sigma
370 * (cos2_sigma_m
371 + big_b / 4.0
372 * (cos_sigma * (-1.0 + 2.0 * cos2_sigma_m_sq)
373 - big_b / 6.0
374 * cos2_sigma_m
375 * (-3.0 + 4.0 * sin_sigma_sq)
376 * (-3.0 + 4.0 * cos2_sigma_m_sq)));
377
378 let distance = b * big_a * (sigma - delta_sigma);
379
380 let sin_lambda = lambda.sin();
385 let cos_lambda = lambda.cos();
386
387 let alpha1 = (cos_u2 * sin_lambda).atan2(cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda);
388 let alpha2_fwd = (cos_u1 * sin_lambda).atan2(-sin_u1 * cos_u2 + cos_u1 * sin_u2 * cos_lambda);
389
390 let alpha2_back_deg = (azi_to_deg(alpha2_fwd) + 180.0) % 360.0;
392
393 Ok(VincentyResult {
394 distance_m: distance,
395 azimuth_fwd_deg: azi_to_deg(alpha1),
396 azimuth_rev_deg: alpha2_back_deg,
397 iterations: iter,
398 })
399}
400
401pub fn vincenty_direct(
418 lat1_deg: f64,
419 lon1_deg: f64,
420 azimuth_fwd_deg: f64,
421 distance_m: f64,
422 params: GeodesicParams,
423) -> Result<VincentyDirectResult, GeodesicError> {
424 validate_lat_lon(lat1_deg, lon1_deg, "starting point")?;
425 let GeodesicParams {
426 a,
427 b,
428 max_iter,
429 tol,
430 } = params;
431
432 if !azimuth_fwd_deg.is_finite() {
433 return Err(GeodesicError::InvalidInput(
434 "azimuth must be a finite number".to_string(),
435 ));
436 }
437 if !distance_m.is_finite() || distance_m < 0.0 {
438 return Err(GeodesicError::InvalidInput(
439 "distance must be a non-negative finite number".to_string(),
440 ));
441 }
442
443 if distance_m < f64::EPSILON {
445 return Ok(VincentyDirectResult {
446 lat2_deg: lat1_deg,
447 lon2_deg: lon1_deg,
448 azimuth_rev_deg: (azimuth_fwd_deg + 180.0) % 360.0,
449 });
450 }
451
452 let f = (a - b) / a;
453
454 let phi1 = lat1_deg.to_radians();
455 let alpha1 = azimuth_fwd_deg.to_radians();
456
457 let sin_alpha1 = alpha1.sin();
458 let cos_alpha1 = alpha1.cos();
459
460 let one_minus_f = 1.0 - f;
461 let tan_u1 = one_minus_f * phi1.tan();
463 let cos_u1 = 1.0 / (1.0 + tan_u1 * tan_u1).sqrt();
464 let sin_u1 = tan_u1 * cos_u1;
465
466 let sigma1 = tan_u1.atan2(cos_alpha1);
468
469 let sin_alpha = cos_u1 * sin_alpha1;
470 let cos2_alpha = 1.0 - sin_alpha * sin_alpha;
471 let u2_sq = cos2_alpha * (a * a - b * b) / (b * b);
472
473 let big_a =
474 1.0 + u2_sq / 16384.0 * (4096.0 + u2_sq * (-768.0 + u2_sq * (320.0 - 175.0 * u2_sq)));
475 let big_b = u2_sq / 1024.0 * (256.0 + u2_sq * (-128.0 + u2_sq * (74.0 - 47.0 * u2_sq)));
476
477 let mut sigma = distance_m / (b * big_a);
479 let mut sigma_prev;
480 let mut iter: u32 = 0;
481
482 let mut cos2_sigma_m;
483 let mut sin_sigma;
484 let mut cos_sigma;
485
486 loop {
487 iter += 1;
488 sigma_prev = sigma;
489
490 cos2_sigma_m = (2.0 * sigma1 + sigma).cos();
491 sin_sigma = sigma.sin();
492 cos_sigma = sigma.cos();
493
494 let cos2_sigma_m_sq = cos2_sigma_m * cos2_sigma_m;
495 let sin_sigma_sq = sin_sigma * sin_sigma;
496
497 let delta_sigma = big_b
498 * sin_sigma
499 * (cos2_sigma_m
500 + big_b / 4.0
501 * (cos_sigma * (-1.0 + 2.0 * cos2_sigma_m_sq)
502 - big_b / 6.0
503 * cos2_sigma_m
504 * (-3.0 + 4.0 * sin_sigma_sq)
505 * (-3.0 + 4.0 * cos2_sigma_m_sq)));
506
507 sigma = distance_m / (b * big_a) + delta_sigma;
508
509 if (sigma - sigma_prev).abs() < tol {
510 break;
511 }
512
513 if iter >= max_iter {
514 return Err(GeodesicError::AntipodalPoints);
515 }
516 }
517
518 cos2_sigma_m = (2.0 * sigma1 + sigma).cos();
520 sin_sigma = sigma.sin();
521 cos_sigma = sigma.cos();
522
523 let num = sin_u1 * cos_sigma + cos_u1 * sin_sigma * cos_alpha1;
525 let denom = one_minus_f
526 * (sin_alpha * sin_alpha + (sin_u1 * sin_sigma - cos_u1 * cos_sigma * cos_alpha1).powi(2))
527 .sqrt();
528 let phi2 = num.atan2(denom);
529
530 let lambda_num = sin_sigma * sin_alpha1;
532 let lambda_den = cos_u1 * cos_sigma - sin_u1 * sin_sigma * cos_alpha1;
533 let lambda_on_sphere = lambda_num.atan2(lambda_den);
534
535 let cos2_sigma_m_sq = cos2_sigma_m * cos2_sigma_m;
536 let c = f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
537
538 let l = lambda_on_sphere
539 - (1.0 - c)
540 * f
541 * sin_alpha
542 * (sigma
543 + c * sin_sigma * (cos2_sigma_m + c * cos_sigma * (-1.0 + 2.0 * cos2_sigma_m_sq)));
544
545 let lon2 = lon1_deg.to_radians() + l;
546
547 let alpha2_fwd = sin_alpha.atan2(-sin_u1 * sin_sigma + cos_u1 * cos_sigma * cos_alpha1);
550 let azimuth_rev_deg = (azi_to_deg(alpha2_fwd) + 180.0) % 360.0;
551
552 Ok(VincentyDirectResult {
553 lat2_deg: phi2.to_degrees(),
554 lon2_deg: lon2.to_degrees(),
555 azimuth_rev_deg,
556 })
557}
558
559pub fn haversine_distance_m(
576 lat1_deg: f64,
577 lon1_deg: f64,
578 lat2_deg: f64,
579 lon2_deg: f64,
580 radius_m: f64,
581) -> f64 {
582 let phi1 = lat1_deg.to_radians();
583 let phi2 = lat2_deg.to_radians();
584 let delta_phi = (lat2_deg - lat1_deg).to_radians();
585 let delta_lambda = (lon2_deg - lon1_deg).to_radians();
586
587 let a = (delta_phi / 2.0).sin().powi(2)
588 + phi1.cos() * phi2.cos() * (delta_lambda / 2.0).sin().powi(2);
589 let c = 2.0 * a.sqrt().asin();
590
591 radius_m * c
592}
593
594pub fn wgs84_inverse(
605 lat1_deg: f64,
606 lon1_deg: f64,
607 lat2_deg: f64,
608 lon2_deg: f64,
609) -> Result<VincentyResult, GeodesicError> {
610 vincenty_inverse(
611 lat1_deg,
612 lon1_deg,
613 lat2_deg,
614 lon2_deg,
615 GeodesicParams::wgs84(),
616 )
617}
618
619pub fn wgs84_direct(
626 lat1_deg: f64,
627 lon1_deg: f64,
628 azimuth_fwd_deg: f64,
629 distance_m: f64,
630) -> Result<VincentyDirectResult, GeodesicError> {
631 vincenty_direct(
632 lat1_deg,
633 lon1_deg,
634 azimuth_fwd_deg,
635 distance_m,
636 GeodesicParams::wgs84(),
637 )
638}
639
640pub fn wgs84_haversine_m(lat1_deg: f64, lon1_deg: f64, lat2_deg: f64, lon2_deg: f64) -> f64 {
645 haversine_distance_m(lat1_deg, lon1_deg, lat2_deg, lon2_deg, WGS84_MEAN_RADIUS)
646}
647
648#[cfg(test)]
653#[allow(clippy::unwrap_used)]
654mod tests {
655 use super::*;
656
657 #[test]
658 fn test_coincident_points_zero_distance() {
659 let result = wgs84_inverse(51.5, -0.1, 51.5, -0.1).unwrap();
660 assert_eq!(result.distance_m, 0.0);
661 assert_eq!(result.iterations, 0);
662 }
663
664 #[test]
665 fn test_azimuth_normalisation() {
666 let azi = azi_to_deg(-core::f64::consts::PI / 4.0);
668 assert!((azi - 315.0).abs() < 1e-9);
669 }
670
671 #[test]
672 fn test_haversine_equatorial() {
673 let d = haversine_distance_m(0.0, 0.0, 0.0, 1.0, WGS84_MEAN_RADIUS);
676 assert!((d - 111_195.0).abs() < 10.0);
677 }
678
679 #[test]
680 fn test_validate_lat_lon_rejects_bad_lat() {
681 let err = validate_lat_lon(91.0, 0.0, "pt");
682 assert!(err.is_err());
683 }
684
685 #[test]
686 fn test_validate_lat_lon_rejects_bad_lon() {
687 let err = validate_lat_lon(0.0, 181.0, "pt");
688 assert!(err.is_err());
689 }
690
691 #[test]
692 fn test_wgs84_constants_consistent() {
693 let f = (WGS84_A - WGS84_B) / WGS84_A;
695 let expected_f = 1.0 / 298.257_223_563;
696 assert!((f - expected_f).abs() < 1e-12);
697 }
698
699 #[test]
700 fn test_direct_zero_distance() {
701 let result = wgs84_direct(48.0, 2.0, 90.0, 0.0).unwrap();
702 assert!((result.lat2_deg - 48.0).abs() < 1e-10);
703 assert!((result.lon2_deg - 2.0).abs() < 1e-10);
704 }
705}