1use crate::coord::GeoCoord;
43use crate::ellipsoid::Ellipsoid;
44use std::f64::consts::PI;
45use thiserror::Error;
46
47#[derive(Debug, Clone, PartialEq, Eq, Error)]
56#[error("Vincenty iteration did not converge after {0} iterations (near-antipodal points)")]
57pub struct VincentyConvergenceError(u32);
58
59impl VincentyConvergenceError {
60 #[inline]
62 pub fn iterations(&self) -> u32 {
63 self.0
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct GeodesicResult {
71 pub distance: f64,
73 pub azimuth_start: f64,
75 pub azimuth_end: f64,
77}
78
79const MAX_ITERATIONS: u32 = 200;
85
86const CONVERGENCE: f64 = 1e-12;
88
89#[inline]
98fn normalize_delta_lon_radians(mut lambda: f64) -> f64 {
99 while lambda > PI {
100 lambda -= 2.0 * PI;
101 }
102 while lambda < -PI {
103 lambda += 2.0 * PI;
104 }
105 lambda
106}
107
108#[inline]
113fn wrap_lon_degrees(mut lon: f64) -> f64 {
114 while lon > 180.0 {
115 lon -= 360.0;
116 }
117 while lon < -180.0 {
118 lon += 360.0;
119 }
120 lon
121}
122
123pub fn geodesic_distance(
133 from: &GeoCoord,
134 to: &GeoCoord,
135) -> Result<GeodesicResult, VincentyConvergenceError> {
136 geodesic_distance_on(from, to, &Ellipsoid::WGS84)
137}
138
139pub fn geodesic_distance_on(
143 from: &GeoCoord,
144 to: &GeoCoord,
145 ellipsoid: &Ellipsoid,
146) -> Result<GeodesicResult, VincentyConvergenceError> {
147 let a = ellipsoid.a;
148 let f = ellipsoid.f;
149 let b = ellipsoid.b;
150
151 let u1 = ((1.0 - f) * from.lat.to_radians().tan()).atan();
153 let u2 = ((1.0 - f) * to.lat.to_radians().tan()).atan();
154 let (sin_u1, cos_u1) = u1.sin_cos();
155 let (sin_u2, cos_u2) = u2.sin_cos();
156
157 let l = normalize_delta_lon_radians((to.lon - from.lon).to_radians());
159 let mut lambda = l;
160
161 let mut sin_sigma;
162 let mut cos_sigma;
163 let mut sigma;
164 let mut sin_alpha;
165 let mut cos2_alpha;
166 let mut cos_2sigma_m;
167
168 for i in 0..MAX_ITERATIONS {
170 let (sin_lambda, cos_lambda) = lambda.sin_cos();
171
172 sin_sigma = ((cos_u2 * sin_lambda).powi(2)
174 + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
175 .sqrt();
176
177 if sin_sigma < 1e-15 {
179 return Ok(GeodesicResult {
180 distance: 0.0,
181 azimuth_start: 0.0,
182 azimuth_end: 0.0,
183 });
184 }
185
186 cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
188 sigma = sin_sigma.atan2(cos_sigma);
190
191 sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma;
193 cos2_alpha = 1.0 - sin_alpha * sin_alpha;
194
195 cos_2sigma_m = if cos2_alpha.abs() < 1e-15 {
197 0.0
198 } else {
199 cos_sigma - 2.0 * sin_u1 * sin_u2 / cos2_alpha
200 };
201
202 let c = f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
204
205 let lambda_prev = lambda;
207 lambda = l
208 + (1.0 - c)
209 * f
210 * sin_alpha
211 * (sigma
212 + c * sin_sigma
213 * (cos_2sigma_m
214 + c * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
215
216 if (lambda - lambda_prev).abs() < CONVERGENCE {
217 let u_sq = cos2_alpha * (a * a - b * b) / (b * b);
221 let cap_a =
223 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
224 let cap_b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
226
227 let delta_sigma = cap_b
229 * sin_sigma
230 * (cos_2sigma_m
231 + cap_b / 4.0
232 * (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
233 - cap_b / 6.0
234 * cos_2sigma_m
235 * (-3.0 + 4.0 * sin_sigma * sin_sigma)
236 * (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
237
238 let distance = b * cap_a * (sigma - delta_sigma);
240
241 let (sin_lam, cos_lam) = lambda.sin_cos();
243 let az_start = (cos_u2 * sin_lam).atan2(cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lam);
244 let az_end = (cos_u1 * sin_lam).atan2(-sin_u1 * cos_u2 + cos_u1 * sin_u2 * cos_lam);
245
246 return Ok(GeodesicResult {
247 distance,
248 azimuth_start: (az_start + 2.0 * PI) % (2.0 * PI),
249 azimuth_end: (az_end + 2.0 * PI) % (2.0 * PI),
250 });
251 }
252
253 if i == MAX_ITERATIONS - 1 {
254 return Err(VincentyConvergenceError(MAX_ITERATIONS));
255 }
256 }
257
258 Err(VincentyConvergenceError(MAX_ITERATIONS))
259}
260
261pub fn geodesic_destination(
272 from: &GeoCoord,
273 azimuth: f64,
274 distance: f64,
275) -> Result<GeoCoord, VincentyConvergenceError> {
276 geodesic_destination_on(from, azimuth, distance, &Ellipsoid::WGS84)
277}
278
279pub fn geodesic_destination_on(
283 from: &GeoCoord,
284 azimuth: f64,
285 distance: f64,
286 ellipsoid: &Ellipsoid,
287) -> Result<GeoCoord, VincentyConvergenceError> {
288 if distance.abs() < 1e-15 {
290 return Ok(*from);
291 }
292
293 let a = ellipsoid.a;
294 let f = ellipsoid.f;
295 let b = ellipsoid.b;
296
297 let (sin_az, cos_az) = azimuth.sin_cos();
298 let tan_u1 = (1.0 - f) * from.lat.to_radians().tan();
299 let cos_u1 = 1.0 / (1.0 + tan_u1 * tan_u1).sqrt();
300 let sin_u1 = tan_u1 * cos_u1;
301
302 let sigma1 = tan_u1.atan2(cos_az);
304 let sin_alpha = cos_u1 * sin_az;
305 let cos2_alpha = 1.0 - sin_alpha * sin_alpha;
306
307 let u_sq = cos2_alpha * (a * a - b * b) / (b * b);
309 let cap_a = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
310 let cap_b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
311
312 let mut sigma = distance / (b * cap_a);
314
315 for i in 0..MAX_ITERATIONS {
317 let cos_2sigma_m = (2.0 * sigma1 + sigma).cos();
318 let sin_sigma = sigma.sin();
319 let cos_sigma = sigma.cos();
320
321 let delta_sigma = cap_b
322 * sin_sigma
323 * (cos_2sigma_m
324 + cap_b / 4.0
325 * (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
326 - cap_b / 6.0
327 * cos_2sigma_m
328 * (-3.0 + 4.0 * sin_sigma * sin_sigma)
329 * (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
330
331 let sigma_prev = sigma;
332 sigma = distance / (b * cap_a) + delta_sigma;
333
334 if (sigma - sigma_prev).abs() < CONVERGENCE {
335 let sin_sigma = sigma.sin();
337 let cos_sigma = sigma.cos();
338 let cos_2sigma_m = (2.0 * sigma1 + sigma).cos();
339
340 let lat2 = (sin_u1 * cos_sigma + cos_u1 * sin_sigma * cos_az).atan2(
341 (1.0 - f)
342 * (sin_alpha * sin_alpha
343 + (sin_u1 * sin_sigma - cos_u1 * cos_sigma * cos_az).powi(2))
344 .sqrt(),
345 );
346
347 let lambda =
348 (sin_sigma * sin_az).atan2(cos_u1 * cos_sigma - sin_u1 * sin_sigma * cos_az);
349 let c = f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
350 let l = lambda
351 - (1.0 - c)
352 * f
353 * sin_alpha
354 * (sigma
355 + c * sin_sigma
356 * (cos_2sigma_m
357 + c * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
358
359 let lon2 = wrap_lon_degrees((from.lon.to_radians() + l).to_degrees());
360
361 return Ok(GeoCoord::new(lat2.to_degrees(), lon2, from.alt));
362 }
363
364 if i == MAX_ITERATIONS - 1 {
365 return Err(VincentyConvergenceError(MAX_ITERATIONS));
366 }
367 }
368
369 Err(VincentyConvergenceError(MAX_ITERATIONS))
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
383 fn same_point_zero_distance() {
384 let p = GeoCoord::from_lat_lon(45.0, 10.0);
385 let result = geodesic_distance(&p, &p).unwrap();
386 assert!(result.distance < 1e-6);
387 }
388
389 #[test]
390 fn known_distance_london_paris() {
391 let london = GeoCoord::from_lat_lon(51.5074, -0.1278);
394 let paris = GeoCoord::from_lat_lon(48.8566, 2.3522);
395 let result = geodesic_distance(&london, &paris).unwrap();
396 assert!((result.distance - 343_500.0).abs() < 1500.0);
397 }
398
399 #[test]
400 fn equatorial_points() {
401 let a = GeoCoord::from_lat_lon(0.0, 0.0);
403 let b = GeoCoord::from_lat_lon(0.0, 1.0);
404 let result = geodesic_distance(&a, &b).unwrap();
405 assert!((result.distance - 111_319.0).abs() < 100.0);
406 }
407
408 #[test]
411 fn inverse_is_symmetric() {
412 let a = GeoCoord::from_lat_lon(40.0, -74.0);
413 let b = GeoCoord::from_lat_lon(51.5, -0.1);
414 let ab = geodesic_distance(&a, &b).unwrap();
415 let ba = geodesic_distance(&b, &a).unwrap();
416 assert!((ab.distance - ba.distance).abs() < 1e-6);
417 }
418
419 #[test]
422 fn inverse_crosses_antimeridian_short_path() {
423 let a = GeoCoord::from_lat_lon(10.0, 179.0);
424 let b = GeoCoord::from_lat_lon(10.0, -179.0);
425 let result = geodesic_distance(&a, &b).unwrap();
426 assert!(result.distance > 100_000.0);
428 assert!(result.distance < 500_000.0);
429 }
430
431 #[test]
434 fn near_antipodal_returns_error() {
435 let a = GeoCoord::from_lat_lon(0.0, 0.0);
436 let b = GeoCoord::from_lat_lon(0.5, 179.7);
437 let result = geodesic_distance(&a, &b);
439 if let Ok(r) = result {
442 assert!(r.distance > 1e7);
443 }
444 }
445
446 #[test]
449 fn direct_roundtrip() {
450 let start = GeoCoord::from_lat_lon(40.0, -74.0);
451 let azimuth = 0.8; let dist = 500_000.0; let dest = geodesic_destination(&start, azimuth, dist).unwrap();
455 let inv = geodesic_distance(&start, &dest).unwrap();
456 assert!((inv.distance - dist).abs() < 0.01);
457 }
458
459 #[test]
460 fn direct_due_north() {
461 let start = GeoCoord::from_lat_lon(0.0, 0.0);
462 let dest = geodesic_destination(&start, 0.0, 1_000_000.0).unwrap();
463 assert!(dest.lat > 8.0 && dest.lat < 10.0);
465 assert!(dest.lon.abs() < 0.001);
466 }
467
468 #[test]
471 fn direct_zero_distance_returns_start() {
472 let start = GeoCoord::from_lat_lon(35.0, 139.0);
473 let dest = geodesic_destination(&start, 1.23, 0.0).unwrap();
474 assert!((dest.lat - start.lat).abs() < 1e-12);
475 assert!((dest.lon - start.lon).abs() < 1e-12);
476 }
477
478 #[test]
481 fn direct_wraps_longitude_range() {
482 let start = GeoCoord::from_lat_lon(0.0, 179.8);
483 let dest = geodesic_destination(&start, PI / 2.0, 100_000.0).unwrap();
484 assert!((-180.0..=180.0).contains(&dest.lon));
485 }
486
487 #[test]
490 fn altitude_passthrough_inverse() {
491 let a = GeoCoord::new(0.0, 0.0, 1234.5);
492 let b = GeoCoord::new(1.0, 1.0, 9999.0);
493 let result = geodesic_distance(&a, &b).unwrap();
494 let a0 = GeoCoord::from_lat_lon(0.0, 0.0);
496 let b0 = GeoCoord::from_lat_lon(1.0, 1.0);
497 let result0 = geodesic_distance(&a0, &b0).unwrap();
498 assert!((result.distance - result0.distance).abs() < 1e-6);
499 }
500
501 #[test]
502 fn altitude_passthrough_direct() {
503 let start = GeoCoord::new(0.0, 0.0, 500.0);
504 let dest = geodesic_destination(&start, 0.0, 100_000.0).unwrap();
505 assert!((dest.alt - 500.0).abs() < 1e-12);
506 }
507}