use geometry_cs::{CoordinateSystem, SphericalFamily};
use geometry_tag::SameAs;
use geometry_trait::Point;
use crate::distance::{DefaultDistance, DistanceStrategy};
#[cfg(feature = "std")]
use crate::normalise::{HasAngularUnits, lonlat_radians};
#[derive(Debug, Clone, Copy)]
pub struct Haversine {
pub radius: f64,
}
impl Haversine {
pub const EARTH: Self = Self {
radius: 6_372_795.0,
};
pub const UNIT: Self = Self { radius: 1.0 };
}
impl Default for Haversine {
#[inline]
fn default() -> Self {
Self::EARTH
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ComparableHaversine;
#[cfg(feature = "std")]
impl<P1, P2> DistanceStrategy<P1, P2> for Haversine
where
P1: Point<Scalar = f64>,
P2: Point<Scalar = f64>,
P1::Cs: HasAngularUnits,
P2::Cs: HasAngularUnits,
<P1::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
<P2::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
{
type Out = f64;
type Comparable = ComparableHaversine;
#[inline]
fn distance(&self, a: &P1, b: &P2) -> Self::Out {
let h = comparable_haversine_h::<P1, P2>(a, b);
2.0 * self.radius * h.sqrt().asin()
}
#[inline]
fn comparable(&self) -> Self::Comparable {
ComparableHaversine
}
}
#[cfg(feature = "std")]
impl<P1, P2> DistanceStrategy<P1, P2> for ComparableHaversine
where
P1: Point<Scalar = f64>,
P2: Point<Scalar = f64>,
P1::Cs: HasAngularUnits,
P2::Cs: HasAngularUnits,
<P1::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
<P2::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
{
type Out = f64;
type Comparable = Self;
#[inline]
fn distance(&self, a: &P1, b: &P2) -> Self::Out {
comparable_haversine_h::<P1, P2>(a, b)
}
#[inline]
fn comparable(&self) -> Self::Comparable {
*self
}
}
impl DefaultDistance<SphericalFamily> for SphericalFamily {
type Strategy = Haversine;
}
#[cfg(feature = "std")]
#[inline]
fn comparable_haversine_h<P1, P2>(a: &P1, b: &P2) -> f64
where
P1: Point<Scalar = f64>,
P2: Point<Scalar = f64>,
P1::Cs: HasAngularUnits,
P2::Cs: HasAngularUnits,
{
let (lon1, lat1) = lonlat_radians(a);
let (lon2, lat2) = lonlat_radians(b);
let dlat_half = (lat2 - lat1) * 0.5;
let dlon_half = (lon2 - lon1) * 0.5;
let s_lat = dlat_half.sin();
let s_lon = dlon_half.sin();
s_lat * s_lat + lat1.cos() * lat2.cos() * s_lon * s_lon
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::{ComparableHaversine, Haversine};
use crate::distance::DistanceStrategy;
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
#[inline]
fn deg(lon: f64, lat: f64) -> WithCs<Adapt<[f64; 2]>, Spherical<Degree>> {
WithCs::new(Adapt([lon, lat]))
}
#[test]
fn amsterdam_paris_average_earth() {
let p1 = deg(4.0, 52.0);
let p2 = deg(2.0, 48.0);
let d = Haversine::EARTH.distance(&p1, &p2);
assert!((d - 467_270.4).abs() < 1.0, "got {d} expected ~ 467270.4");
}
#[test]
fn symmetric_in_arguments() {
let p1 = deg(4.0, 52.0);
let p2 = deg(2.0, 48.0);
let ab = Haversine::EARTH.distance(&p1, &p2);
let ba = Haversine::EARTH.distance(&p2, &p1);
assert!((ab - ba).abs() < 1e-6);
}
#[test]
fn unit_sphere_returns_radians() {
let p1 = deg(4.0, 52.0);
let p2 = deg(2.0, 48.0);
let angle = Haversine::UNIT.distance(&p1, &p2);
let expected = 467_270.4 / 6_372_795.0;
assert!((angle - expected).abs() < 1e-6);
}
#[test]
fn amsterdam_barcelona() {
let p1 = deg(4.0, 52.0);
let p2 = deg(2.0, 41.0);
let d = Haversine::EARTH.distance(&p1, &p2);
assert!(
(d - 1_232_906.5).abs() < 1.0,
"got {d} expected ~ 1232906.5"
);
}
#[test]
fn comparable_orders_match_distance() {
let o = deg(0.0, 0.0);
let near = deg(1.0, 0.0);
let far = deg(45.0, 0.0);
let h_near = ComparableHaversine.distance(&o, &near);
let h_far = ComparableHaversine.distance(&o, &far);
assert!(h_near < h_far);
let d_near = Haversine::EARTH.distance(&o, &near);
let d_far = Haversine::EARTH.distance(&o, &far);
assert!(d_near < d_far);
}
#[test]
fn quickstart_amsterdam_paris_in_miles() {
let ams = deg(4.90, 52.37);
let par = deg(2.35, 48.86);
let angle = Haversine::UNIT.distance(&ams, &par);
let miles = angle * 3959.0;
assert!(
(miles - 267.02).abs() < 0.05,
"got {miles} expected ~ 267.02"
);
}
fn _accepts_readonly_point<P, S>(s: &S, a: &P, b: &P) -> S::Out
where
P: geometry_trait::Point,
S: DistanceStrategy<P, P>,
{
s.distance(a, b)
}
type GP = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
#[test]
fn comparable_yields_the_h_term() {
let a = deg(4.0, 52.0);
let b = deg(2.0, 48.0);
let full = Haversine::UNIT.distance(&a, &b);
let h = DistanceStrategy::<GP, GP>::comparable(&Haversine::UNIT).distance(&a, &b);
let reconstructed = 2.0 * h.sqrt().asin();
assert!(
(full - reconstructed).abs() < 1e-12,
"{full} vs {reconstructed}"
);
}
#[test]
fn comparable_of_comparable_is_itself_and_order_preserving() {
let o = deg(0.0, 0.0);
let near = deg(1.0, 0.0);
let far = deg(10.0, 0.0);
let cmp = DistanceStrategy::<GP, GP>::comparable(&ComparableHaversine);
assert!(cmp.distance(&o, &near) < cmp.distance(&o, &far));
}
#[test]
#[allow(
clippy::used_underscore_items,
reason = "the test exists to run the compile-time witness's body"
)]
fn readonly_witness_computes_distance() {
let d = _accepts_readonly_point(&Haversine::UNIT, °(0.0, 0.0), °(1.0, 0.0));
assert!(d > 0.0, "got {d}");
}
}