use geometry_cs::{CoordinateSystem, GeographicFamily, Spheroid};
use geometry_tag::SameAs;
use geometry_trait::Point;
use crate::distance::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 Thomas {
pub spheroid: Spheroid,
}
impl Thomas {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
};
}
impl Default for Thomas {
#[inline]
fn default() -> Self {
Self::WGS84
}
}
#[cfg(feature = "std")]
impl<P1, P2> DistanceStrategy<P1, P2> for Thomas
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::similar_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 f = calc.f;
let one_minus_f = 1.0 - f;
let pi_half = core::f64::consts::FRAC_PI_2;
let theta1 = if lat1 == pi_half || lat1 == -pi_half {
lat1
} else {
(one_minus_f * lat1.tan()).atan()
};
let theta2 = if lat2 == pi_half || lat2 == -pi_half {
lat2
} else {
(one_minus_f * lat2.tan()).atan()
};
let theta_m = f64::midpoint(theta1, theta2);
let d_theta_m = (theta2 - theta1) / 2.0;
let d_lambda = lon2 - lon1;
let d_lambda_m = d_lambda / 2.0;
let sin_theta_m = theta_m.sin();
let cos_theta_m = theta_m.cos();
let sin_d_theta_m = d_theta_m.sin();
let cos_d_theta_m = d_theta_m.cos();
let sin2_theta_m = sin_theta_m * sin_theta_m;
let cos2_theta_m = cos_theta_m * cos_theta_m;
let sin2_d_theta_m = sin_d_theta_m * sin_d_theta_m;
let cos2_d_theta_m = cos_d_theta_m * cos_d_theta_m;
let sin_d_lambda_m = d_lambda_m.sin();
let sin2_d_lambda_m = sin_d_lambda_m * sin_d_lambda_m;
let upper_h = cos2_theta_m - sin2_d_theta_m;
let l_term = sin2_d_theta_m + upper_h * sin2_d_lambda_m;
let cos_d = (1.0 - 2.0 * l_term).clamp(-1.0, 1.0);
let d = cos_d.acos();
let sin_d = d.sin();
let one_minus_l = 1.0 - l_term;
if sin_d == 0.0 || l_term == 0.0 || one_minus_l == 0.0 {
return 0.0;
}
let upper_u = 2.0 * sin2_theta_m * cos2_d_theta_m / one_minus_l;
let upper_v = 2.0 * sin2_d_theta_m * cos2_theta_m / l_term;
let upper_x = upper_u + upper_v;
let upper_y = upper_u - upper_v;
let upper_t = d / sin_d;
let upper_d = 4.0 * upper_t * upper_t;
let upper_e = 2.0 * cos_d;
let upper_a = upper_d * upper_e;
let upper_b = 2.0 * upper_d;
let upper_c = upper_t - (upper_a - upper_e) / 2.0;
let f_sqr = f * f;
let f_sqr_per_64 = f_sqr / 64.0;
let n1 = upper_x * (upper_a + upper_c * upper_x);
let n2 = upper_y * (upper_b + upper_e * upper_y);
let n3 = upper_d * upper_x * upper_y;
let delta1d = f * (upper_t * upper_x - upper_y) / 4.0;
let delta2d = f_sqr_per_64 * (n1 - n2 + n3);
calc.a * sin_d * (upper_t - delta1d + delta2d)
}
#[inline]
fn comparable(&self) -> Self::Comparable {
*self
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::Thomas;
use crate::distance::DistanceStrategy;
use crate::geographic::Andoyer;
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_north_1deg_lon_10deg_lat() {
let d = Thomas::WGS84.distance(°(0.0, 90.0), °(1.0, 80.0));
assert!((d / 1000.0 - 1_116.825_795).abs() < 0.012);
}
#[test]
fn polar_south_1deg_lon_10deg_lat() {
let d = Thomas::WGS84.distance(°(0.0, -90.0), °(1.0, -80.0));
assert!((d / 1000.0 - 1_116.825_795).abs() < 0.012);
}
#[test]
fn zero_distance_on_equal_points() {
let p = deg(4.0, 52.0);
let d = Thomas::WGS84.distance(&p, &p);
assert!(d.abs() < 1e-6, "got {d}");
}
#[test]
fn lon_4_lat_52_to_lon_3_lat_40() {
let d = Thomas::WGS84.distance(°(4.0, 52.0), °(3.0, 40.0));
assert!((d / 1000.0 - 1_336.025_365).abs() < 0.014);
}
#[test]
fn thomas_within_50m_of_andoyer_amsterdam_to_madrid() {
let a = deg(4.0, 52.0);
let b = deg(3.0, 40.0);
let t = Thomas::WGS84.distance(&a, &b);
let an = Andoyer::WGS84.distance(&a, &b);
assert!((t - an).abs() < 50.0);
}
#[test]
fn default_is_wgs84() {
let a = Thomas::default();
let w = Thomas::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)
}
#[test]
fn comparable_produces_the_same_distance() {
let a = deg(4.0, 52.0);
let b = deg(3.0, 40.0);
let real = Thomas::WGS84.distance(&a, &b);
let cmp = DistanceStrategy::<GP, GP>::comparable(&Thomas::WGS84).distance(&a, &b);
assert!((real - cmp).abs() < 1e-9);
}
#[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(&Thomas::WGS84, °(4.0, 52.0), °(3.0, 40.0));
assert!(d > 1_000_000.0, "≈1336 km, got {d}");
}
}