use geometry_cs::CoordinateSystem;
use geometry_strategy::{AzimuthStrategy, DefaultAzimuth, DefaultAzimuthStrategy};
use geometry_trait::Point;
type Family<P> = <<P as Point>::Cs as CoordinateSystem>::Family;
#[must_use]
pub fn azimuth<P1, P2>(
p1: &P1,
p2: &P2,
) -> <DefaultAzimuthStrategy<P1> as AzimuthStrategy<P1, P2>>::Out
where
P1: Point,
P2: Point,
Family<P1>: DefaultAzimuth<Family<P1>>,
DefaultAzimuthStrategy<P1>: AzimuthStrategy<P1, P2> + Default,
{
DefaultAzimuthStrategy::<P1>::default().azimuth(p1, p2)
}
#[must_use]
#[allow(
clippy::needless_pass_by_value,
reason = "Strategies are ZST/small Copy configuration objects; by-value matches the user-facing call shape."
)]
pub fn azimuth_with<P1, P2, S>(p1: &P1, p2: &P2, s: S) -> S::Out
where
P1: Point,
P2: Point,
S: AzimuthStrategy<P1, P2>,
{
s.azimuth(p1, p2)
}
#[cfg(test)]
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::azimuth;
use geometry_cs::Cartesian;
use geometry_model::Point2D;
type P = Point2D<f64, Cartesian>;
#[test]
fn due_north_is_zero() {
assert!((azimuth(&P::new(0.0, 0.0), &P::new(0.0, 1.0)) - 0.0).abs() < 1e-12);
}
#[test]
fn due_east_is_half_pi() {
let a = azimuth(&P::new(0.0, 0.0), &P::new(1.0, 0.0));
assert!((a - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
}
#[cfg(feature = "std")]
#[test]
fn spherical_diagonal_close_to_45() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
let got = azimuth(&sp(0., 0.), &sp(1., 1.));
let expected = 44.995_636_455_344_851_f64.to_radians();
assert!((got - expected).abs() < 1e-9, "got {got}");
}
#[cfg(feature = "std")]
#[test]
fn geographic_diagonal_close_to_45() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
let got = 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}");
}
#[cfg(feature = "std")]
#[test]
fn geographic_destination_at_a_pole() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
let north = azimuth(&gg(10., 20.), &gg(10., 90.));
assert!(north.abs() < 1e-12, "got {north}");
let south = azimuth(&gg(10., 20.), &gg(10., -90.));
assert!((south - core::f64::consts::PI).abs() < 1e-12, "got {south}");
}
#[cfg(feature = "std")]
#[test]
fn geographic_start_at_north_pole_points_south() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
let got = azimuth(&gg(0., 90.), &gg(0., 0.));
assert!((got - core::f64::consts::PI).abs() < 1e-12, "got {got}");
}
#[cfg(feature = "std")]
#[test]
fn geographic_antimeridian_crossing_heads_east() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
let got = azimuth(&gg(179.5, 0.), &gg(-179.5, 0.));
assert!(
(got - core::f64::consts::FRAC_PI_2).abs() < 1e-6,
"got {got}"
);
}
}