use geometry_cs::{CoordinateSystem, GeographicFamily, Spheroid};
use geometry_tag::SameAs;
use geometry_trait::Point;
use crate::azimuth::AzimuthStrategy;
#[cfg(feature = "std")]
use crate::geographic::spheroid_calc::SpheroidCalc;
#[cfg(feature = "std")]
use crate::normalise::{HasAngularUnits, lonlat_radians};
#[derive(Debug, Clone, Copy)]
pub struct GeographicAzimuth {
pub spheroid: Spheroid,
}
impl GeographicAzimuth {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
};
}
impl Default for GeographicAzimuth {
#[inline]
fn default() -> Self {
Self::WGS84
}
}
#[cfg(feature = "std")]
impl<P1, P2> AzimuthStrategy<P1, P2> for GeographicAzimuth
where
P1: Point<Scalar = f64>,
P2: Point<Scalar = f64>,
P1::Cs: HasAngularUnits,
P2::Cs: HasAngularUnits,
<P1::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
<P2::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
{
type Out = f64;
#[allow(clippy::many_single_char_names, clippy::float_cmp)]
#[inline]
fn azimuth(&self, p1: &P1, p2: &P2) -> f64 {
let calc = SpheroidCalc::from(self.spheroid);
let f = calc.f;
let (lon1, lat1) = lonlat_radians(p1);
let (lon2, lat2) = lonlat_radians(p2);
let dlon = lon2 - lon1;
let sin_dlon = dlon.sin();
let cos_dlon = dlon.cos();
let sin_lat1 = lat1.sin();
let cos_lat1 = lat1.cos();
let sin_lat2 = lat2.sin();
let cos_lat2 = lat2.cos();
let cos_d = (sin_lat1 * sin_lat2 + cos_lat1 * cos_lat2 * cos_dlon).clamp(-1.0, 1.0);
let d = cos_d.acos();
let sin_d = d.sin();
if sin_d.abs() <= f64::EPSILON {
return 0.0;
}
let pi = core::f64::consts::PI;
let (a, u) = if cos_lat2.abs() <= f64::EPSILON {
(if sin_lat2 < 0.0 { pi } else { 0.0 }, 0.0)
} else {
let tan_lat2 = sin_lat2 / cos_lat2;
let m = cos_lat1 * tan_lat2 - sin_lat1 * cos_dlon;
let a = sin_dlon.atan2(m);
let u = (f / 2.0) * (cos_lat1 * cos_lat1) * (2.0 * a).sin();
(a, u)
};
let v = if cos_lat1.abs() <= f64::EPSILON {
0.0
} else {
let tan_lat1 = sin_lat1 / cos_lat1;
let n = cos_lat2 * tan_lat1 - sin_lat2 * cos_dlon;
let b = sin_dlon.atan2(n);
(f / 2.0) * (cos_lat2 * cos_lat2) * (2.0 * b).sin()
};
let t = d / sin_d;
let da = v * t - u;
let mut azimuth = a - da;
normalize_azimuth(&mut azimuth, a, da);
azimuth
}
}
#[cfg(feature = "std")]
#[inline]
fn normalize_azimuth(azimuth: &mut f64, a: f64, da: f64) {
let pi = core::f64::consts::PI;
if a >= 0.0 {
if da >= 0.0 {
if *azimuth < 0.0 {
*azimuth = 0.0;
}
} else if *azimuth > pi {
*azimuth = pi;
}
} else {
if da <= 0.0 {
if *azimuth > 0.0 {
*azimuth = 0.0;
}
} else if *azimuth < -pi {
*azimuth = -pi;
}
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
#![allow(
clippy::float_cmp,
reason = "azimuths are compared with an explicit absolute tolerance, not `==`"
)]
#![allow(
clippy::excessive_precision,
reason = "reference constants are copied verbatim from Boost's azimuth.cpp; the trailing digits document the source value even past f64 precision."
)]
use super::GeographicAzimuth;
use crate::azimuth::AzimuthStrategy;
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
#[inline]
fn gg(lon: f64, lat: f64) -> Gg {
WithCs::new(Adapt([lon, lat]))
}
#[test]
fn coincident_is_zero() {
let got = GeographicAzimuth::WGS84.azimuth(&gg(0., 0.), &gg(0., 0.));
assert!(got.abs() < 1e-12);
}
#[test]
fn diagonal() {
let got = GeographicAzimuth::WGS84.azimuth(&gg(0., 0.), &gg(1., 1.));
let expected = 45.187_718_848_049_521_f64.to_radians();
assert!((got - expected).abs() < 1e-9, "got {got}");
}
#[test]
fn shallow_east() {
let got = GeographicAzimuth::WGS84.azimuth(&gg(0., 0.), &gg(100., 1.));
let expected = 88.986_933_066_023_497_f64.to_radians();
assert!((got - expected).abs() < 1e-9, "got {got}");
}
#[test]
fn diagonal_west() {
let got = GeographicAzimuth::WGS84.azimuth(&gg(0., 0.), &gg(-1., 1.));
let expected = (-45.187_718_848_049_521_f64).to_radians();
assert!((got - expected).abs() < 1e-9, "got {got}");
}
#[test]
fn normalize_azimuth_clamps_all_four_quadrants() {
use super::normalize_azimuth;
let pi = core::f64::consts::PI;
let mut az = -0.1;
normalize_azimuth(&mut az, 0.05, 0.15);
assert_eq!(az, 0.0);
let mut az = pi + 0.1;
normalize_azimuth(&mut az, pi - 0.05, -0.15);
assert_eq!(az, pi);
let mut az = 0.1;
normalize_azimuth(&mut az, -0.05, -0.15);
assert_eq!(az, 0.0);
let mut az = -pi - 0.1;
normalize_azimuth(&mut az, -pi + 0.05, 0.15);
assert_eq!(az, -pi);
let mut az = 0.5;
normalize_azimuth(&mut az, 0.4, -0.1);
assert_eq!(az, 0.5);
let mut az = -0.5;
normalize_azimuth(&mut az, -0.4, -0.1);
assert_eq!(az, -0.5);
}
}