1use std::f64::consts::{PI, TAU};
30
31use serde::{Deserialize, Serialize};
32
33use geographiclib_rs::{DirectGeodesic, Geodesic as Solver, InverseGeodesic};
34
35use crate::error::CoreError;
36use crate::geodesy::{Ellipsoid, Geodetic};
37
38pub const GEODESIC_MAX_FLATTENING: f64 = 1.0 / 150.0;
43
44#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
46#[non_exhaustive]
47pub struct GeodesicInverse {
48 pub distance_m: f64,
50 pub initial_azimuth_rad: f64,
53 pub final_azimuth_rad: f64,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
63#[non_exhaustive]
64pub struct GeodesicDirect {
65 pub latitude_rad: f64,
67 pub longitude_rad: f64,
69 pub final_azimuth_rad: f64,
71}
72
73impl GeodesicDirect {
74 pub fn end(&self, height_m: f64) -> Result<Geodetic, CoreError> {
80 Geodetic::new(self.latitude_rad, self.longitude_rad, height_m)
81 }
82}
83
84fn degrees(angle_rad: f64) -> f64 {
87 if angle_rad.abs() <= PI {
88 angle_rad.to_degrees()
89 } else {
90 (angle_rad % TAU).to_degrees()
91 }
92}
93
94impl Ellipsoid {
95 fn geodesic_solver(&self) -> Result<Solver, CoreError> {
96 if self.flattening() > GEODESIC_MAX_FLATTENING {
97 return Err(CoreError::Domain {
98 what: "flattening for geodesics (at most 1/150)",
99 value: self.flattening(),
100 });
101 }
102 Ok(Solver::new(self.semi_major_axis_m(), self.flattening()))
103 }
104
105 pub fn geodesic_inverse(
133 &self,
134 from: Geodetic,
135 to: Geodetic,
136 ) -> Result<GeodesicInverse, CoreError> {
137 let solver = self.geodesic_solver()?;
138 let (from, to) = (from.validated()?, to.validated()?);
139 let (distance_m, azi1_deg, azi2_deg, _arc_deg) = solver.inverse(
140 from.latitude_rad.to_degrees(),
141 degrees(from.longitude_rad),
142 to.latitude_rad.to_degrees(),
143 degrees(to.longitude_rad),
144 );
145 Ok(GeodesicInverse {
146 distance_m,
147 initial_azimuth_rad: azi1_deg.to_radians(),
148 final_azimuth_rad: azi2_deg.to_radians(),
149 })
150 }
151
152 pub fn geodesic_direct(
175 &self,
176 from: Geodetic,
177 azimuth_rad: f64,
178 distance_m: f64,
179 ) -> Result<GeodesicDirect, CoreError> {
180 let solver = self.geodesic_solver()?;
181 let from = from.validated()?;
182 if !azimuth_rad.is_finite() {
183 return Err(CoreError::Domain {
184 what: "geodesic azimuth (rad)",
185 value: azimuth_rad,
186 });
187 }
188 if !distance_m.is_finite() {
189 return Err(CoreError::Domain {
190 what: "geodesic distance (m)",
191 value: distance_m,
192 });
193 }
194 let (lat2_deg, lon2_deg, azi2_deg) = solver.direct(
195 from.latitude_rad.to_degrees(),
196 degrees(from.longitude_rad),
197 degrees(azimuth_rad),
198 distance_m,
199 );
200 if !(lat2_deg.is_finite() && lon2_deg.is_finite() && azi2_deg.is_finite()) {
201 return Err(CoreError::Domain {
203 what: "geodesic distance (m)",
204 value: distance_m,
205 });
206 }
207 Ok(GeodesicDirect {
208 latitude_rad: lat2_deg.to_radians(),
209 longitude_rad: lon2_deg.to_radians(),
210 final_azimuth_rad: azi2_deg.to_radians(),
211 })
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::f64::consts::{FRAC_PI_2, PI};
218
219 use proptest::prelude::*;
220
221 use super::*;
222
223 const WGS84: Ellipsoid = Ellipsoid::WGS84;
224
225 fn point(latitude_deg: f64, longitude_deg: f64) -> Geodetic {
226 Geodetic::from_degrees(latitude_deg, longitude_deg, 0.0).expect("a valid point")
227 }
228
229 #[test]
230 fn on_a_sphere_the_distance_is_the_great_circle_arc() {
231 let sphere = Ellipsoid::new(6_371_000.0, f64::INFINITY).expect("a sphere");
234 for (from, to) in [
235 (point(10.0, 20.0), point(-35.0, 140.0)),
236 (point(60.0, -5.0), point(61.0, 2.5)),
237 (point(-80.0, 0.0), point(0.0, -170.0)),
238 ] {
239 let (p1, p2, dl) = (
240 from.latitude_rad,
241 to.latitude_rad,
242 to.longitude_rad - from.longitude_rad,
243 );
244 let h =
245 ((p2 - p1) / 2.0).sin().powi(2) + p1.cos() * p2.cos() * (dl / 2.0).sin().powi(2);
246 let arc = 2.0 * h.sqrt().asin();
247 let azimuth =
248 (dl.sin() * p2.cos()).atan2(p1.cos() * p2.sin() - p1.sin() * p2.cos() * dl.cos());
249 let g = sphere.geodesic_inverse(from, to).expect("a geodesic");
250 assert!((g.distance_m - 6_371_000.0 * arc).abs() < 1e-8, "{g:?}");
251 assert!((g.initial_azimuth_rad - azimuth).abs() < 1e-14, "{g:?}");
252 }
253 }
254
255 #[test]
257 fn the_guides_worked_example() {
258 let pad = point(32.990_4, -106.975_0);
259 let landing = point(33.000_0, -106.968_0);
260 let g = WGS84.geodesic_inverse(pad, landing).expect("a geodesic");
261 assert!((g.distance_m - 1_249.614).abs() < 5e-4, "{g:?}");
263 assert!(
264 (g.initial_azimuth_rad.to_degrees() - 31.567).abs() < 5e-4,
265 "{g:?}"
266 );
267 assert!(
268 (g.final_azimuth_rad.to_degrees() - 31.571).abs() < 5e-4,
269 "{g:?}"
270 );
271 let mean = 0.5 * (pad.latitude_rad + landing.latitude_rad);
275 let (a, e2) = (WGS84.semi_major_axis_m(), WGS84.eccentricity_squared());
276 let w = (1.0 - e2 * mean.sin().powi(2)).sqrt();
277 let north = a * (1.0 - e2) / w.powi(3) * (landing.latitude_rad - pad.latitude_rad);
278 let east = a / w * mean.cos() * (landing.longitude_rad - pad.longitude_rad);
279 assert!((north.hypot(east) - g.distance_m).abs() < 1e-3);
280 let d = WGS84
282 .geodesic_direct(pad, 60f64.to_radians(), 2_000.0)
283 .expect("finite");
284 assert!(
285 (d.latitude_rad.to_degrees() - 32.999_415).abs() < 5e-7,
286 "{d:?}"
287 );
288 assert!(
289 (d.longitude_rad.to_degrees() + 106.956_466).abs() < 5e-7,
290 "{d:?}"
291 );
292 assert!(
293 (d.final_azimuth_rad.to_degrees() - 60.010).abs() < 5e-4,
294 "{d:?}"
295 );
296 }
297
298 #[test]
299 fn coincident_points_are_zero_apart() {
300 let p = point(32.99, -106.97);
301 assert_eq!(
302 WGS84.geodesic_inverse(p, p).expect("a geodesic").distance_m,
303 0.0
304 );
305 }
306
307 #[test]
308 fn a_meridian_runs_due_north_at_both_ends() {
309 let g = WGS84
310 .geodesic_inverse(point(0.0, 10.0), point(45.0, 10.0))
311 .expect("a geodesic");
312 assert_eq!(g.initial_azimuth_rad, 0.0);
313 assert_eq!(g.final_azimuth_rad, 0.0);
314 let d = WGS84
315 .geodesic_direct(point(0.0, 10.0), 0.0, g.distance_m)
316 .expect("finite");
317 assert!((d.latitude_rad.to_degrees() - 45.0).abs() < 1e-13);
318 }
319
320 #[test]
321 fn heights_are_ignored() {
322 let low = point(40.0, -100.0);
323 let high = Geodetic::from_degrees(40.0, -100.0, 3_000.0).expect("a valid point");
324 let to = point(41.0, -99.0);
325 assert_eq!(
326 WGS84.geodesic_inverse(low, to).expect("a geodesic"),
327 WGS84.geodesic_inverse(high, to).expect("a geodesic")
328 );
329 let d = WGS84.geodesic_direct(high, 1.0, 5_000.0).expect("finite");
330 assert_eq!(d.end(0.0).expect("a position").height_m, 0.0);
331 assert_eq!(d, WGS84.geodesic_direct(low, 1.0, 5_000.0).expect("finite"));
332 }
333
334 #[test]
335 fn a_negative_distance_runs_backwards() {
336 let from = point(20.0, 30.0);
337 let ahead = WGS84
338 .geodesic_direct(from, 0.7, -250_000.0)
339 .expect("finite");
340 let behind = WGS84
341 .geodesic_direct(from, 0.7 - PI, 250_000.0)
342 .expect("finite");
343 assert!((ahead.latitude_rad - behind.latitude_rad).abs() < 1e-14);
344 assert!((ahead.longitude_rad - behind.longitude_rad).abs() < 1e-14);
345 }
346
347 #[test]
348 fn refuses_flattenings_past_one_in_150() {
349 let a = point(10.0, 20.0);
350 let b = point(-35.0, 140.0);
351 let edge = Ellipsoid::from_flattening(6.4e6, GEODESIC_MAX_FLATTENING).expect("valid");
352 assert!(edge.geodesic_inverse(a, b).is_ok());
353 for f in [GEODESIC_MAX_FLATTENING.next_up(), 0.2] {
354 let past = Ellipsoid::from_flattening(6.4e6, f).expect("a valid ellipsoid");
355 for result in [
356 past.geodesic_inverse(a, b).map(|_| ()),
357 past.geodesic_direct(a, 1.0, 1e6).map(|_| ()),
358 ] {
359 match result {
360 Err(CoreError::Domain { what, value }) => {
361 assert_eq!(what, "flattening for geodesics (at most 1/150)");
362 assert_eq!(value, f);
363 }
364 other => panic!("{other:?}"),
365 }
366 }
367 }
368 }
369
370 #[test]
371 fn refuses_points_built_out_of_range() {
372 let bad = Geodetic {
373 latitude_rad: 2.0,
374 longitude_rad: 0.0,
375 height_m: 0.0,
376 };
377 let good = point(0.0, 0.0);
378 for result in [
379 WGS84.geodesic_inverse(bad, good).map(|_| ()),
380 WGS84.geodesic_inverse(good, bad).map(|_| ()),
381 WGS84.geodesic_direct(bad, 1.0, 1.0).map(|_| ()),
382 ] {
383 match result {
384 Err(CoreError::Domain { what, value }) => {
385 assert_eq!(what, "geodetic latitude (rad)");
386 assert_eq!(value, 2.0);
387 }
388 other => panic!("{other:?}"),
389 }
390 }
391 }
392
393 #[test]
394 fn a_longitude_of_many_turns_is_reduced() {
395 let far = Geodetic::new(0.5, 1e307, 0.0).expect("a valid point");
397 let g = WGS84
398 .geodesic_inverse(far, point(0.0, 0.0))
399 .expect("a geodesic");
400 assert!(g.distance_m.is_finite() && g.initial_azimuth_rad.is_finite());
401 let d = WGS84.geodesic_direct(far, 1e300, 1_000.0).expect("finite");
402 assert!(d.longitude_rad.is_finite() && d.final_azimuth_rad.is_finite());
403 let east = Geodetic::new(0.0, 1.5 * PI, 0.0).expect("a valid point");
405 let west = Geodetic::new(0.0, -0.5 * PI, 0.0).expect("a valid point");
406 let to = point(10.0, 0.0);
407 let (ge, gw) = (
408 WGS84.geodesic_inverse(east, to).expect("a geodesic"),
409 WGS84.geodesic_inverse(west, to).expect("a geodesic"),
410 );
411 assert!((ge.distance_m - gw.distance_m).abs() < 1e-8);
412 }
413
414 #[test]
415 fn results_round_trip_through_serde() {
416 let g = WGS84
417 .geodesic_inverse(point(1.0, 2.0), point(3.0, 4.0))
418 .expect("a geodesic");
419 let text = serde_json::to_string(&g).expect("serializes");
420 assert_eq!(
421 serde_json::from_str::<GeodesicInverse>(&text).expect("reads"),
422 g
423 );
424 let d = WGS84
425 .geodesic_direct(point(1.0, 2.0), 0.3, 5e5)
426 .expect("finite");
427 let text = serde_json::to_string(&d).expect("serializes");
428 assert_eq!(
429 serde_json::from_str::<GeodesicDirect>(&text).expect("reads"),
430 d
431 );
432 }
433
434 #[test]
435 fn direct_refuses_non_finite_inputs() {
436 let from = point(0.0, 0.0);
437 for (azimuth, distance, what) in [
438 (f64::NAN, 1.0, "geodesic azimuth (rad)"),
439 (f64::INFINITY, 1.0, "geodesic azimuth (rad)"),
440 (0.0, f64::NAN, "geodesic distance (m)"),
441 (0.0, f64::NEG_INFINITY, "geodesic distance (m)"),
442 ] {
443 match WGS84.geodesic_direct(from, azimuth, distance) {
444 Err(CoreError::Domain { what: w, .. }) => assert_eq!(w, what),
445 other => panic!("{other:?}"),
446 }
447 }
448 }
449
450 proptest! {
451 #![proptest_config(ProptestConfig::with_cases(256))]
452
453 #[test]
456 fn inverse_then_direct_lands_on_the_second_point(
457 lat1 in -FRAC_PI_2..=FRAC_PI_2,
458 lon1 in -10.0..10.0f64,
459 lat2 in -FRAC_PI_2..=FRAC_PI_2,
460 lon2 in -10.0..10.0f64,
461 ) {
462 let from = Geodetic::new(lat1, lon1, 0.0).expect("a valid point");
463 let to = Geodetic::new(lat2, lon2, 0.0).expect("a valid point");
464 let g = WGS84.geodesic_inverse(from, to).expect("a geodesic");
465 prop_assert!(g.distance_m >= 0.0 && g.distance_m <= 20_003_931.46);
467 prop_assert!(g.initial_azimuth_rad.abs() <= PI && g.final_azimuth_rad.abs() <= PI);
468 let d = WGS84
469 .geodesic_direct(from, g.initial_azimuth_rad, g.distance_m)
470 .expect("finite");
471 prop_assert!(d.longitude_rad.abs() <= PI && d.final_azimuth_rad.abs() <= PI);
472 let miss = WGS84.ecef_from_geodetic(d.end(0.0).expect("a position")) - WGS84.ecef_from_geodetic(to);
473 prop_assert!(miss.length() < 1e-7, "missed by {} m", miss.length());
474 }
475 }
476}