geometry_strategy/spherical/
azimuth.rs1use geometry_cs::{CoordinateSystem, SphericalFamily};
25use geometry_tag::SameAs;
26use geometry_trait::Point;
27
28use crate::azimuth::AzimuthStrategy;
29
30#[cfg(feature = "std")]
31use crate::normalise::{HasAngularUnits, lonlat_radians};
32
33#[derive(Debug, Default, Clone, Copy)]
40pub struct SphericalAzimuth;
41
42#[cfg(feature = "std")]
43impl<P1, P2> AzimuthStrategy<P1, P2> for SphericalAzimuth
44where
45 P1: Point<Scalar = f64>,
46 P2: Point<Scalar = f64>,
47 P1::Cs: HasAngularUnits,
48 P2::Cs: HasAngularUnits,
49 <P1::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
50 <P2::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
51{
52 type Out = f64;
53
54 #[inline]
55 fn azimuth(&self, p1: &P1, p2: &P2) -> f64 {
56 let (lon1, lat1) = lonlat_radians(p1);
57 let (lon2, lat2) = lonlat_radians(p2);
58 let dlon = lon2 - lon1;
59 let y = dlon.sin() * lat2.cos();
62 let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
63 y.atan2(x)
64 }
65}
66
67#[cfg(all(test, feature = "std"))]
68mod tests {
69 #![allow(
73 clippy::float_cmp,
74 reason = "azimuths are compared with an explicit absolute tolerance, not `==`"
75 )]
76 #![allow(
77 clippy::excessive_precision,
78 reason = "reference constants are copied verbatim from Boost's azimuth.cpp; the trailing digits document the source value even past f64 precision."
79 )]
80
81 use super::SphericalAzimuth;
82 use crate::azimuth::AzimuthStrategy;
83 use geometry_adapt::{Adapt, WithCs};
84 use geometry_cs::{Degree, Spherical};
85
86 type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
87
88 #[inline]
89 fn sp(lon: f64, lat: f64) -> Sp {
90 WithCs::new(Adapt([lon, lat]))
91 }
92
93 #[test]
95 fn coincident_is_zero() {
96 let got = SphericalAzimuth.azimuth(&sp(0., 0.), &sp(0., 0.));
97 assert!(got.abs() < 1e-12);
98 }
99
100 #[test]
102 fn diagonal() {
103 let got = SphericalAzimuth.azimuth(&sp(0., 0.), &sp(1., 1.));
104 let expected = 44.995_636_455_344_851_f64.to_radians();
105 assert!((got - expected).abs() < 1e-9, "got {got}");
106 }
107
108 #[test]
110 fn shallow_east() {
111 let got = SphericalAzimuth.azimuth(&sp(0., 0.), &sp(100., 1.));
112 let expected = 88.984_576_593_576_293_f64.to_radians();
113 assert!((got - expected).abs() < 1e-9, "got {got}");
114 }
115
116 #[test]
118 fn diagonal_west() {
119 let got = SphericalAzimuth.azimuth(&sp(0., 0.), &sp(-1., 1.));
120 let expected = (-44.995_636_455_344_851_f64).to_radians();
121 assert!((got - expected).abs() < 1e-9, "got {got}");
122 }
123}