use geometry_cs::{CoordinateSystem, GeographicFamily, Spheroid};
use geometry_tag::SameAs;
use geometry_trait::Point;
use crate::distance::{DefaultDistance, DistanceStrategy};
#[cfg(feature = "std")]
use crate::geographic::spheroid_calc::SpheroidCalc;
#[cfg(feature = "std")]
use crate::normalise::{HasAngularUnits, lonlat_radians};
#[derive(Debug, Clone, Copy)]
pub struct Andoyer {
pub spheroid: Spheroid,
}
impl Andoyer {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
};
}
impl Default for Andoyer {
#[inline]
fn default() -> Self {
Self::WGS84
}
}
#[cfg(feature = "std")]
impl<P1, P2> DistanceStrategy<P1, P2> for Andoyer
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;
type Comparable = Self;
#[allow(clippy::many_single_char_names, clippy::float_cmp)]
#[inline]
fn distance(&self, a: &P1, b: &P2) -> Self::Out {
let calc = SpheroidCalc::from(self.spheroid);
let (lon1, lat1) = lonlat_radians(a);
let (lon2, lat2) = lonlat_radians(b);
if lon1 == lon2 && lat1 == lat2 {
return 0.0;
}
let dlon = lon2 - lon1;
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();
let k = (sin_lat1 - sin_lat2) * (sin_lat1 - sin_lat2);
let l = (sin_lat1 + sin_lat2) * (sin_lat1 + sin_lat2);
let three_sin_d = 3.0 * sin_d;
let one_minus_cos_d = 1.0 - cos_d;
let one_plus_cos_d = 1.0 + cos_d;
let h = if one_minus_cos_d == 0.0 {
0.0
} else {
(d + three_sin_d) / one_minus_cos_d
};
let g = if one_plus_cos_d == 0.0 {
0.0
} else {
(d - three_sin_d) / one_plus_cos_d
};
let dd = -(calc.f / 4.0) * (h * k + g * l);
calc.a * (d + dd)
}
#[inline]
fn comparable(&self) -> Self::Comparable {
*self
}
}
impl DefaultDistance<GeographicFamily> for GeographicFamily {
type Strategy = Andoyer;
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::Andoyer;
use crate::distance::DistanceStrategy;
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type GP = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
#[inline]
fn deg(lon: f64, lat: f64) -> GP {
WithCs::new(Adapt([lon, lat]))
}
#[test]
fn polar_1deg_lon_10deg_lat() {
let d = Andoyer::WGS84.distance(°(0.0, 90.0), °(1.0, 80.0));
assert!(
(d / 1000.0 - 1_116.814_237).abs() < 0.01,
"{} km expected ~ 1116.814 km",
d / 1000.0
);
}
#[test]
fn zero_distance_on_equal_points() {
let p = deg(4.0, 52.0);
let d = Andoyer::WGS84.distance(&p, &p);
assert!(d.abs() < 1e-3, "got {d}");
}
#[test]
fn lon_4_lat_52_to_lon_3_lat_40() {
let d = Andoyer::WGS84.distance(°(4.0, 52.0), °(3.0, 40.0));
assert!(
(d / 1000.0 - 1_336.039_890).abs() < 0.01,
"{} km expected ~ 1336.040 km",
d / 1000.0
);
}
#[test]
fn antipodal_equatorial() {
let expected_km = core::f64::consts::PI * 6_378_137.0 / 1000.0;
for (a, b) in [
(deg(0.0, 0.0), deg(180.0, 0.0)),
(deg(0.0, 0.0), deg(-180.0, 0.0)),
(deg(-90.0, 0.0), deg(90.0, 0.0)),
(deg(90.0, 0.0), deg(-90.0, 0.0)),
] {
let d = Andoyer::WGS84.distance(&a, &b);
assert!(
(d / 1000.0 - expected_km).abs() < 1.0,
"got {} km, expected ~ {} km",
d / 1000.0,
expected_km,
);
}
}
#[test]
fn default_is_wgs84() {
let a = Andoyer::default();
let w = Andoyer::WGS84;
assert_eq!(a.spheroid, w.spheroid);
}
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)
}
}