use crate::error::{SpatialError, SpatialResult};
use std::f64::consts::PI;
pub const WGS84_A: f64 = 6_378_137.0;
pub const WGS84_F: f64 = 1.0 / 298.257_223_563;
pub const WGS84_B: f64 = WGS84_A * (1.0 - WGS84_F);
pub const EARTH_R: f64 = 6_371_008.8;
#[inline]
fn to_rad(d: f64) -> f64 {
d * PI / 180.0
}
#[inline]
fn to_deg(r: f64) -> f64 {
r * 180.0 / PI
}
#[inline]
fn wrap_pi(a: f64) -> f64 {
let mut v = a % (2.0 * PI);
if v > PI {
v -= 2.0 * PI;
} else if v < -PI {
v += 2.0 * PI;
}
v
}
#[inline]
fn wrap_360(a: f64) -> f64 {
((a % 360.0) + 360.0) % 360.0
}
#[derive(Debug, Clone, Copy)]
pub struct VincentyResult {
pub distance: f64,
pub initial_bearing: f64,
pub final_bearing: f64,
}
pub fn vincenty_inverse(
lat1: f64,
lon1: f64,
lat2: f64,
lon2: f64,
) -> SpatialResult<VincentyResult> {
let phi1 = to_rad(lat1);
let phi2 = to_rad(lat2);
let l = to_rad(lon2 - lon1);
let u1 = ((1.0 - WGS84_F) * phi1.tan()).atan();
let u2 = ((1.0 - WGS84_F) * phi2.tan()).atan();
let sin_u1 = u1.sin();
let cos_u1 = u1.cos();
let sin_u2 = u2.sin();
let cos_u2 = u2.cos();
let mut lam = l;
let mut lam_prev;
let mut sin_sigma = 0.0_f64;
let mut cos_sigma = 0.0_f64;
let mut sigma = 0.0_f64;
let mut sin_alpha = 0.0_f64;
let mut cos2_alpha = 0.0_f64;
let mut cos_2sigma_m = 0.0_f64;
let mut converged = false;
for _ in 0..1000 {
sin_sigma = ((cos_u2 * lam.sin()).powi(2)
+ (cos_u1 * sin_u2 - sin_u1 * cos_u2 * lam.cos()).powi(2))
.sqrt();
cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * lam.cos();
sigma = sin_sigma.atan2(cos_sigma);
sin_alpha = if sin_sigma.abs() < 1e-15 {
0.0
} else {
cos_u1 * cos_u2 * lam.sin() / sin_sigma
};
cos2_alpha = 1.0 - sin_alpha * sin_alpha;
cos_2sigma_m = if cos2_alpha.abs() < 1e-15 {
0.0 } else {
cos_sigma - 2.0 * sin_u1 * sin_u2 / cos2_alpha
};
let c = WGS84_F / 16.0 * cos2_alpha * (4.0 + WGS84_F * (4.0 - 3.0 * cos2_alpha));
lam_prev = lam;
lam = l
+ (1.0 - c)
* WGS84_F
* sin_alpha
* (sigma
+ c * sin_sigma
* (cos_2sigma_m
+ c * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
if (lam - lam_prev).abs() < 1e-12 {
converged = true;
break;
}
}
if !converged {
return Err(SpatialError::ComputationError(
"Vincenty inverse failed to converge (near-antipodal points)".to_string(),
));
}
let u2_sq = cos2_alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
let k1 = ((1.0 + u2_sq).sqrt() - 1.0) / ((1.0 + u2_sq).sqrt() + 1.0);
let a_coeff = (1.0 + k1 * k1 / 4.0) / (1.0 - k1);
let b_coeff = k1 * (1.0 - 3.0 * k1 * k1 / 8.0);
let delta_sigma = b_coeff
* sin_sigma
* (cos_2sigma_m
+ b_coeff / 4.0
* (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
- b_coeff / 6.0
* cos_2sigma_m
* (-3.0 + 4.0 * sin_sigma * sin_sigma)
* (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
let s = WGS84_B * a_coeff * (sigma - delta_sigma);
let az1 = to_deg(wrap_pi(
(cos_u2 * lam.sin()).atan2(cos_u1 * sin_u2 - sin_u1 * cos_u2 * lam.cos()),
));
let az2 = to_deg(wrap_pi(
(cos_u1 * lam.sin()).atan2(-sin_u1 * cos_u2 + cos_u1 * sin_u2 * lam.cos()),
));
Ok(VincentyResult {
distance: s,
initial_bearing: wrap_360(az1),
final_bearing: wrap_360(az2 + 180.0),
})
}
pub fn vincenty_direct(
lat1: f64,
lon1: f64,
bearing_deg: f64,
distance: f64,
) -> SpatialResult<(f64, f64, f64)> {
let phi1 = to_rad(lat1);
let alpha1 = to_rad(bearing_deg);
let u1 = ((1.0 - WGS84_F) * phi1.tan()).atan();
let sigma1 = u1.tan().atan2(alpha1.cos());
let sin_alpha = u1.cos() * alpha1.sin();
let cos2_alpha = 1.0 - sin_alpha * sin_alpha;
let u2_sq = cos2_alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
let k1 = ((1.0 + u2_sq).sqrt() - 1.0) / ((1.0 + u2_sq).sqrt() + 1.0);
let a_coeff = (1.0 + k1 * k1 / 4.0) / (1.0 - k1);
let b_coeff = k1 * (1.0 - 3.0 * k1 * k1 / 8.0);
let mut sigma = distance / (WGS84_B * a_coeff);
let mut sigma_prev;
for _ in 0..1000 {
let cos_2sigma_m = (2.0 * sigma1 + sigma).cos();
let delta_sigma = b_coeff
* sigma.sin()
* (cos_2sigma_m
+ b_coeff / 4.0
* (sigma.cos() * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
- b_coeff / 6.0
* cos_2sigma_m
* (-3.0 + 4.0 * sigma.sin().powi(2))
* (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
sigma_prev = sigma;
sigma = distance / (WGS84_B * a_coeff) + delta_sigma;
if (sigma - sigma_prev).abs() < 1e-12 {
break;
}
}
let cos_2sigma_m = (2.0 * sigma1 + sigma).cos();
let sin_u1 = u1.sin();
let cos_u1 = u1.cos();
let lat2 = (sin_u1 * sigma.cos() + cos_u1 * sigma.sin() * alpha1.cos()).atan2(
(1.0 - WGS84_F)
* (sin_alpha.powi(2)
+ (sin_u1 * sigma.sin() - cos_u1 * sigma.cos() * alpha1.cos()).powi(2))
.sqrt(),
);
let lam = (sigma.sin() * alpha1.sin()).atan2(
cos_u1 * sigma.cos() - sin_u1 * sigma.sin() * alpha1.cos(),
);
let c = WGS84_F / 16.0 * cos2_alpha * (4.0 + WGS84_F * (4.0 - 3.0 * cos2_alpha));
let lon2 = to_rad(lon1)
+ lam
- (1.0 - c)
* WGS84_F
* sin_alpha
* (sigma
+ c * sigma.sin()
* (cos_2sigma_m
+ c * sigma.cos()
* (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
let az2 = alpha1.sin().atan2(-sin_u1 * sigma.sin() + cos_u1 * sigma.cos() * alpha1.cos());
Ok((
to_deg(lat2),
to_deg(lon2),
wrap_360(to_deg(az2) + 180.0),
))
}
pub fn haversine(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let phi1 = to_rad(lat1);
let phi2 = to_rad(lat2);
let dphi = to_rad(lat2 - lat1);
let dlam = to_rad(lon2 - lon1);
let a = (dphi / 2.0).sin().powi(2) + phi1.cos() * phi2.cos() * (dlam / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_R * c
}
pub fn rhumb_bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let dphi = to_rad(lat2) - to_rad(lat1);
let dlam = to_rad(lon2 - lon1);
let dpsi = {
let psi1 = ((PI / 4.0 + to_rad(lat1) / 2.0).tan()).ln();
let psi2 = ((PI / 4.0 + to_rad(lat2) / 2.0).tan()).ln();
psi2 - psi1
};
let dlam_adj = if dlam.abs() > PI {
if dlam > 0.0 {
dlam - 2.0 * PI
} else {
dlam + 2.0 * PI
}
} else {
dlam
};
wrap_360(to_deg(dlam_adj.atan2(dpsi)))
}
pub fn rhumb_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let dphi = to_rad(lat2 - lat1);
let dlam = to_rad(lon2 - lon1);
let q = if dphi.abs() < 1e-12 {
to_rad(lat1).cos()
} else {
let psi1 = ((PI / 4.0 + to_rad(lat1) / 2.0).tan()).ln();
let psi2 = ((PI / 4.0 + to_rad(lat2) / 2.0).tan()).ln();
dphi / (psi2 - psi1)
};
let dlam_adj = if dlam.abs() > PI {
if dlam > 0.0 {
dlam - 2.0 * PI
} else {
dlam + 2.0 * PI
}
} else {
dlam
};
EARTH_R * (dphi.powi(2) + (q * dlam_adj).powi(2)).sqrt()
}
pub fn rhumb_destination(lat: f64, lon: f64, bearing_deg: f64, distance: f64) -> (f64, f64) {
let phi1 = to_rad(lat);
let lam1 = to_rad(lon);
let theta = to_rad(bearing_deg);
let delta = distance / EARTH_R;
let dphi = delta * theta.cos();
let phi2 = phi1 + dphi;
let phi2_clamped = phi2.clamp(-PI / 2.0 + 1e-10, PI / 2.0 - 1e-10);
let dpsi = if dphi.abs() < 1e-12 {
phi1.cos()
} else {
let psi1 = ((PI / 4.0 + phi1 / 2.0).tan()).ln();
let psi2 = ((PI / 4.0 + phi2_clamped / 2.0).tan()).ln();
if (psi2 - psi1).abs() < 1e-12 {
phi1.cos()
} else {
dphi / (psi2 - psi1)
}
};
let dlam = delta * theta.sin() / dpsi;
let lam2 = ((lam1 + dlam + PI) % (2.0 * PI)) - PI;
(to_deg(phi2_clamped), to_deg(lam2))
}
pub fn initial_bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let phi1 = to_rad(lat1);
let phi2 = to_rad(lat2);
let dlam = to_rad(lon2 - lon1);
let y = dlam.sin() * phi2.cos();
let x = phi1.cos() * phi2.sin() - phi1.sin() * phi2.cos() * dlam.cos();
wrap_360(to_deg(y.atan2(x)))
}
pub fn final_bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
wrap_360(initial_bearing(lat2, lon2, lat1, lon1))
}
pub fn midpoint(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> (f64, f64) {
let phi1 = to_rad(lat1);
let phi2 = to_rad(lat2);
let dlam = to_rad(lon2 - lon1);
let lam1 = to_rad(lon1);
let bx = phi2.cos() * dlam.cos();
let by = phi2.cos() * dlam.sin();
let phi_m = (phi1.sin() + phi2.sin()).atan2(((phi1.cos() + bx).powi(2) + by * by).sqrt());
let lam_m = lam1 + by.atan2(phi1.cos() + bx);
(to_deg(phi_m), to_deg(lam_m))
}
pub fn destination_point(lat: f64, lon: f64, bearing_deg: f64, distance: f64) -> (f64, f64) {
let phi1 = to_rad(lat);
let lam1 = to_rad(lon);
let theta = to_rad(bearing_deg);
let delta = distance / EARTH_R;
let phi2 =
(phi1.sin() * delta.cos() + phi1.cos() * delta.sin() * theta.cos()).asin();
let lam2 = lam1
+ (theta.sin() * delta.sin() * phi1.cos())
.atan2(delta.cos() - phi1.sin() * phi2.sin());
(to_deg(phi2), to_deg(lam2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vincenty_inverse_london_paris() {
let r = vincenty_inverse(51.5074, -0.1278, 48.8566, 2.3522).expect("vincenty");
assert!((r.distance - 340_000.0).abs() < 5_000.0, "dist={}", r.distance);
}
#[test]
fn test_vincenty_inverse_same_point() {
let r = vincenty_inverse(40.0, -74.0, 40.0, -74.0).expect("vincenty");
assert!(r.distance < 1.0, "same point distance={}", r.distance);
}
#[test]
fn test_vincenty_direct_roundtrip() {
let (lat2, lon2, _) = vincenty_direct(51.5074, -0.1278, 108.0, 200_000.0).expect("direct");
let r = vincenty_inverse(51.5074, -0.1278, lat2, lon2).expect("inverse");
assert!((r.distance - 200_000.0).abs() < 1.0, "dist={}", r.distance);
}
#[test]
fn test_haversine_known_distances() {
let d = haversine(51.5074, -0.1278, 48.8566, 2.3522);
assert!((d - 343_000.0).abs() < 5_000.0, "d={d}");
assert!(haversine(0.0, 0.0, 0.0, 0.0) < 1e-6);
}
#[test]
fn test_initial_bearing_cardinal() {
assert!((initial_bearing(0.0, 0.0, 1.0, 0.0)).abs() < 0.01); assert!((initial_bearing(0.0, 0.0, 0.0, 1.0) - 90.0).abs() < 0.01); assert!((initial_bearing(1.0, 0.0, 0.0, 0.0) - 180.0).abs() < 0.01); assert!((initial_bearing(0.0, 1.0, 0.0, 0.0) - 270.0).abs() < 0.01); }
#[test]
fn test_final_bearing() {
let fb = final_bearing(0.0, 0.0, 10.0, 0.0);
assert!((fb - 180.0).abs() < 0.1, "fb={fb}");
}
#[test]
fn test_midpoint_equatorial() {
let (lat_m, lon_m) = midpoint(0.0, 0.0, 0.0, 20.0);
assert!(lat_m.abs() < 1e-6, "lat={lat_m}");
assert!((lon_m - 10.0).abs() < 1e-6, "lon={lon_m}");
}
#[test]
fn test_midpoint_symmetric() {
let (m1_lat, m1_lon) = midpoint(10.0, 20.0, 30.0, 40.0);
let (m2_lat, m2_lon) = midpoint(30.0, 40.0, 10.0, 20.0);
assert!((m1_lat - m2_lat).abs() < 1e-8, "lat symmetry");
assert!((m1_lon - m2_lon).abs() < 1e-8, "lon symmetry");
}
#[test]
fn test_destination_point_north() {
let (lat2, lon2) = destination_point(0.0, 0.0, 0.0, 100_000.0);
assert!(lat2 > 0.0, "lat2={lat2}");
assert!(lon2.abs() < 0.001, "lon2={lon2}");
}
#[test]
fn test_destination_roundtrip() {
let (lat2, lon2) = destination_point(40.0, -74.0, 45.0, 500_000.0);
let b = initial_bearing(40.0, -74.0, lat2, lon2);
assert!((b - 45.0).abs() < 0.1, "bearing={b}");
let d = haversine(40.0, -74.0, lat2, lon2);
assert!((d - 500_000.0).abs() < 1000.0, "dist={d}");
}
#[test]
fn test_rhumb_bearing_east() {
let b = rhumb_bearing(0.0, 0.0, 0.0, 10.0);
assert!((b - 90.0).abs() < 0.01, "b={b}");
}
#[test]
fn test_rhumb_bearing_north() {
let b = rhumb_bearing(0.0, 0.0, 10.0, 0.0);
assert!(b < 0.01 || (b - 360.0).abs() < 0.01, "b={b}");
}
#[test]
fn test_rhumb_distance_sanity() {
let d = rhumb_distance(0.0, 0.0, 0.0, 10.0);
assert!((d - 1_111_000.0).abs() < 20_000.0, "d={d}");
}
#[test]
fn test_rhumb_destination_roundtrip() {
let bearing = 45.0;
let dist = 300_000.0;
let (lat2, lon2) = rhumb_destination(10.0, 20.0, bearing, dist);
let b_back = rhumb_bearing(10.0, 20.0, lat2, lon2);
assert!((b_back - bearing).abs() < 0.01, "b_back={b_back}");
let d_back = rhumb_distance(10.0, 20.0, lat2, lon2);
assert!((d_back - dist).abs() < 100.0, "d_back={d_back}");
}
}