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 Vincenty {
pub spheroid: Spheroid,
pub max_iterations: u32,
pub tolerance: f64,
}
impl Vincenty {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
max_iterations: 1000,
tolerance: 1e-12,
};
}
impl Default for Vincenty {
#[inline]
fn default() -> Self {
Self::WGS84
}
}
#[cfg(feature = "std")]
impl<P1, P2> DistanceStrategy<P1, P2> for Vincenty
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,
clippy::similar_names
)]
#[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 pi = core::f64::consts::PI;
let two_pi = 2.0 * pi;
let mut big_l = lon2 - lon1;
if big_l < -pi {
big_l += two_pi;
}
if big_l > pi {
big_l -= two_pi;
}
let mut lambda = big_l;
let f = calc.f;
let a_radius = calc.a;
let b_radius = calc.b;
let one_min_f = 1.0 - f;
let tan_u1 = one_min_f * lat1.tan();
let tan_u2 = one_min_f * lat2.tan();
let cos_u1 = 1.0 / (1.0 + tan_u1 * tan_u1).sqrt();
let cos_u2 = 1.0 / (1.0 + tan_u2 * tan_u2).sqrt();
let sin_u1 = tan_u1 * cos_u1;
let sin_u2 = tan_u2 * cos_u2;
let mut sin_sigma;
let mut cos_sigma;
let mut sigma;
let mut sin_alpha;
let mut cos2_alpha;
let mut cos_2sigma_m;
let mut cos2_2sigma_m;
let mut counter: u32 = 0;
loop {
let previous_lambda = lambda;
let sin_lambda = lambda.sin();
let cos_lambda = lambda.cos();
let sx = cos_u2 * sin_lambda;
let sy = cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda;
sin_sigma = (sx * sx + sy * sy).sqrt();
cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
sin_alpha = if sin_sigma == 0.0 {
0.0
} else {
cos_u1 * cos_u2 * sin_lambda / sin_sigma
};
cos2_alpha = 1.0 - sin_alpha * sin_alpha;
cos_2sigma_m = if cos2_alpha == 0.0 {
0.0
} else {
cos_sigma - 2.0 * sin_u1 * sin_u2 / cos2_alpha
};
cos2_2sigma_m = cos_2sigma_m * cos_2sigma_m;
let c = f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
sigma = sin_sigma.atan2(cos_sigma);
lambda = big_l
+ (1.0 - c)
* f
* sin_alpha
* (sigma
+ c * sin_sigma
* (cos_2sigma_m + c * cos_sigma * (-1.0 + 2.0 * cos2_2sigma_m)));
counter += 1;
let converged = (previous_lambda - lambda).abs() <= self.tolerance;
if converged || lambda.abs() >= pi || counter >= self.max_iterations {
break;
}
}
let a_over_b = a_radius / b_radius;
let u_sq = cos2_alpha * (a_over_b * a_over_b - 1.0);
let big_a =
1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
let big_b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
let cos_sigma_final = sigma.cos();
let sin2_sigma = sin_sigma * sin_sigma;
let delta_sigma = big_b
* sin_sigma
* (cos_2sigma_m
+ (big_b / 4.0)
* (cos_sigma_final * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
- (big_b / 6.0)
* cos_2sigma_m
* (-3.0 + 4.0 * sin2_sigma)
* (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
b_radius * big_a * (sigma - delta_sigma)
}
#[inline]
fn comparable(&self) -> Self::Comparable {
*self
}
}
#[cfg(all(test, feature = "std"))]
#[allow(
clippy::doc_markdown,
reason = "doc-comments quote `1_336_039.890` etc. — Rust numeric \
literal syntax which clippy flags as missing backticks; \
wrapping them in extra backticks hurts readability for \
what is plainly a numeric value with its units."
)]
mod tests {
use super::Vincenty;
use crate::distance::DistanceStrategy;
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic, Spheroid};
type GP = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
#[inline]
fn deg(lon: f64, lat: f64) -> GP {
WithCs::new(Adapt([lon, lat]))
}
const GDA: Spheroid = Spheroid {
equatorial_radius: 6_378_137.0,
flattening: 1.0 / 298.257_222_10,
};
fn gda_vincenty() -> Vincenty {
Vincenty {
spheroid: GDA,
..Vincenty::WGS84
}
}
#[test]
fn meridian_north_50_degrees() {
let d = gda_vincenty().distance(°(0.0, 0.0), °(0.0, 50.0));
assert!(
(d - 5_540_847.042).abs() < 1e-2,
"got {d} m, expected ~ 5_540_847.042 m"
);
}
#[test]
fn equator_east_50_degrees() {
let d = gda_vincenty().distance(°(0.0, 0.0), °(50.0, 0.0));
assert!(
(d - 5_565_974.540).abs() < 1e-2,
"got {d} m, expected ~ 5_565_974.540 m"
);
}
#[test]
fn northeast_50_50() {
let d = gda_vincenty().distance(°(0.0, 0.0), °(50.0, 50.0));
assert!(
(d - 7_284_879.297).abs() < 1e-2,
"got {d} m, expected ~ 7_284_879.297 m"
);
}
#[test]
fn sub_polar() {
let d = Vincenty::WGS84.distance(°(0.0, 89.0), °(1.0, 80.0));
let tolerance = 1_005_153.576_9 * 0.001 / 100.0;
assert!(
(d - 1_005_153.576_9).abs() < tolerance,
"got {d} m, expected ~ 1_005_153.576_9 m (tol {tolerance} m)"
);
}
#[test]
fn identity_zero() {
let p = deg(4.0, 52.0);
let d = gda_vincenty().distance(&p, &p);
assert!(d.abs() < 1e-6, "got {d}");
}
#[test]
fn lon_4_lat_52_to_lon_3_lat_40() {
let d = Vincenty::WGS84.distance(°(4.0, 52.0), °(3.0, 40.0));
let tolerance = 1_336_039.890 * 0.001 / 100.0;
assert!(
(d - 1_336_039.890).abs() < tolerance,
"got {d} m, expected ~ 1_336_039.890 m (tol {tolerance} m)"
);
}
#[test]
fn lodz_to_trondheim() {
let lodz = deg(19.0 + 28.0 / 60.0, 51.0 + 47.0 / 60.0);
let trondheim = deg(10.0 + 21.0 / 60.0, 63.0 + 23.0 / 60.0);
let d = gda_vincenty().distance(&lodz, &trondheim);
assert!(
(d - 1_399_032.724).abs() < 1.0,
"got {d} m, expected ~ 1_399_032.724 m"
);
}
#[test]
fn london_to_new_york() {
let london = deg(
0.0 + 7.0 / 60.0 + 39.0 / 3600.0,
51.0 + 30.0 / 60.0 + 26.0 / 3600.0,
);
let nyc = deg(
-(74.0 + 0.0 / 60.0 + 21.0 / 3600.0),
40.0 + 42.0 / 60.0 + 46.0 / 3600.0,
);
let d = gda_vincenty().distance(&london, &nyc);
assert!(
(d - 5_602_044.851).abs() < 1.0,
"got {d} m, expected ~ 5_602_044.851 m"
);
}
#[test]
fn default_is_wgs84() {
let v = Vincenty::default();
let w = Vincenty::WGS84;
assert_eq!(v.spheroid, w.spheroid);
assert_eq!(v.max_iterations, w.max_iterations);
assert!((v.tolerance - w.tolerance).abs() < 1e-30);
}
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)
}
}