use std::f64::consts::PI;
use thiserror::Error;
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 WGS84_E2: f64 = 2.0 * WGS84_F - WGS84_F * WGS84_F;
pub const WGS84_EP2: f64 = (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
#[derive(Debug, Clone, PartialEq, Error)]
pub enum GeoError {
#[error("Vincenty iteration did not converge after {max_iter} iterations (lambda change = {delta:.3e}); points may be nearly antipodal")]
VincentyNonConvergence {
max_iter: u32,
delta: f64,
},
#[error("Invalid coordinate value for '{field}': {reason}")]
InvalidCoordinate {
field: &'static str,
reason: String,
},
#[error("Domain error: {0}")]
DomainError(String),
}
pub type GeoResult<T> = Result<T, GeoError>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GeographicCoord {
pub lat: f64,
pub lon: f64,
pub alt: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct EcefCoord {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct EnuCoord {
pub east: f64,
pub north: f64,
pub up: f64,
}
#[inline]
fn deg2rad(deg: f64) -> f64 {
deg * PI / 180.0
}
#[inline]
fn rad2deg(rad: f64) -> f64 {
rad * 180.0 / PI
}
#[inline]
fn prime_vertical_radius(sin_lat: f64) -> f64 {
WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt()
}
impl GeographicCoord {
#[inline]
pub fn new(lat: f64, lon: f64, alt: f64) -> Self {
Self { lat, lon, alt }
}
pub fn to_ecef(&self) -> EcefCoord {
let lat_r = deg2rad(self.lat);
let lon_r = deg2rad(self.lon);
let sin_lat = lat_r.sin();
let cos_lat = lat_r.cos();
let sin_lon = lon_r.sin();
let cos_lon = lon_r.cos();
let n = prime_vertical_radius(sin_lat);
EcefCoord {
x: (n + self.alt) * cos_lat * cos_lon,
y: (n + self.alt) * cos_lat * sin_lon,
z: (n * (1.0 - WGS84_E2) + self.alt) * sin_lat,
}
}
pub fn to_enu(&self, origin: &GeographicCoord) -> EnuCoord {
let origin_ecef = origin.to_ecef();
let point_ecef = self.to_ecef();
point_ecef.to_enu(&origin_ecef)
}
pub fn vincenty_distance(&self, other: &GeographicCoord) -> GeoResult<f64> {
const MAX_ITER: u32 = 200;
const TOLERANCE: f64 = 1e-12;
let lat1 = deg2rad(self.lat);
let lat2 = deg2rad(other.lat);
let l = deg2rad(other.lon - self.lon);
let u1 = ((1.0 - WGS84_F) * lat1.tan()).atan();
let u2 = ((1.0 - WGS84_F) * lat2.tan()).atan();
let sin_u1 = u1.sin();
let cos_u1 = u1.cos();
let sin_u2 = u2.sin();
let cos_u2 = u2.cos();
let mut lambda = l;
let mut lambda_prev;
let mut sin_sigma;
let mut cos_sigma;
let mut sigma;
let mut sin_alpha;
let mut cos2_alpha;
let mut cos2_sigma_m;
let mut iter = 0_u32;
loop {
let sin_lambda = lambda.sin();
let cos_lambda = lambda.cos();
sin_sigma = ((cos_u2 * sin_lambda).powi(2)
+ (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
.sqrt();
cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
sigma = sin_sigma.atan2(cos_sigma);
sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma.max(f64::EPSILON);
cos2_alpha = 1.0 - sin_alpha * sin_alpha;
cos2_sigma_m = if cos2_alpha.abs() < f64::EPSILON {
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));
lambda_prev = lambda;
lambda = l
+ (1.0 - c)
* WGS84_F
* sin_alpha
* (sigma
+ c * sin_sigma
* (cos2_sigma_m
+ c * cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)));
iter += 1;
if (lambda - lambda_prev).abs() < TOLERANCE {
break;
}
if iter >= MAX_ITER {
return Err(GeoError::VincentyNonConvergence {
max_iter: MAX_ITER,
delta: (lambda - lambda_prev).abs(),
});
}
}
let u2_coeff = cos2_alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
let big_a =
1.0 + u2_coeff / 16384.0 * (4096.0 + u2_coeff * (-768.0 + u2_coeff * (320.0 - 175.0 * u2_coeff)));
let big_b = u2_coeff / 1024.0 * (256.0 + u2_coeff * (-128.0 + u2_coeff * (74.0 - 47.0 * u2_coeff)));
let delta_sigma = big_b
* sin_sigma
* (cos2_sigma_m
+ big_b / 4.0
* (cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)
- big_b / 6.0
* cos2_sigma_m
* (-3.0 + 4.0 * sin_sigma * sin_sigma)
* (-3.0 + 4.0 * cos2_sigma_m * cos2_sigma_m)));
Ok(WGS84_B * big_a * (sigma - delta_sigma))
}
pub fn haversine_distance(&self, other: &GeographicCoord) -> f64 {
let lat1 = deg2rad(self.lat);
let lat2 = deg2rad(other.lat);
let dlat = lat2 - lat1;
let dlon = deg2rad(other.lon - self.lon);
let a = (dlat / 2.0).sin().powi(2)
+ lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
WGS84_A * c
}
pub fn bearing_to(&self, other: &GeographicCoord) -> f64 {
let lat1 = deg2rad(self.lat);
let lat2 = deg2rad(other.lat);
let dlon = deg2rad(other.lon - self.lon);
let y = dlon.sin() * lat2.cos();
let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
let bearing_rad = y.atan2(x);
let bearing_deg = rad2deg(bearing_rad);
(bearing_deg + 360.0) % 360.0
}
pub fn vincenty_bearing(&self, other: &GeographicCoord) -> GeoResult<f64> {
const MAX_ITER: u32 = 200;
const TOLERANCE: f64 = 1e-12;
let lat1 = deg2rad(self.lat);
let lat2 = deg2rad(other.lat);
let l = deg2rad(other.lon - self.lon);
let u1 = ((1.0 - WGS84_F) * lat1.tan()).atan();
let u2 = ((1.0 - WGS84_F) * lat2.tan()).atan();
let sin_u1 = u1.sin();
let cos_u1 = u1.cos();
let sin_u2 = u2.sin();
let cos_u2 = u2.cos();
let mut lambda = l;
let mut iter = 0_u32;
loop {
let sin_lambda = lambda.sin();
let cos_lambda = lambda.cos();
let sin_sigma = ((cos_u2 * sin_lambda).powi(2)
+ (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
.sqrt();
let cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma.max(f64::EPSILON);
let cos2_alpha = 1.0 - sin_alpha * sin_alpha;
let cos2_sigma_m = if cos2_alpha.abs() < f64::EPSILON {
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));
let lambda_prev = lambda;
lambda = l
+ (1.0 - c)
* WGS84_F
* sin_alpha
* (cos_sigma.asin()
+ c * sin_sigma
* (cos2_sigma_m
+ c * cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)));
iter += 1;
if (lambda - lambda_prev).abs() < TOLERANCE {
let alpha1 = (cos_u2 * sin_lambda).atan2(
cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda,
);
let bearing_deg = rad2deg(alpha1);
return Ok((bearing_deg + 360.0) % 360.0);
}
if iter >= MAX_ITER {
return Err(GeoError::VincentyNonConvergence {
max_iter: MAX_ITER,
delta: (lambda - lambda_prev).abs(),
});
}
}
}
pub fn destination(&self, distance: f64, bearing: f64) -> GeoResult<GeographicCoord> {
if distance < 0.0 {
return Err(GeoError::DomainError(format!(
"distance must be non-negative, got {distance}"
)));
}
const MAX_ITER: u32 = 200;
const TOLERANCE: f64 = 1e-12;
let alpha1 = deg2rad(bearing);
let sin_alpha1 = alpha1.sin();
let cos_alpha1 = alpha1.cos();
let tan_u1 = (1.0 - WGS84_F) * deg2rad(self.lat).tan();
let cos_u1 = 1.0 / (1.0 + tan_u1 * tan_u1).sqrt();
let sin_u1 = tan_u1 * cos_u1;
let sigma1 = tan_u1.atan2(cos_alpha1);
let sin_alpha = cos_u1 * sin_alpha1;
let cos2_alpha = 1.0 - sin_alpha * sin_alpha;
let u2 = cos2_alpha * WGS84_EP2;
let big_a = 1.0
+ u2 / 16384.0
* (4096.0 + u2 * (-768.0 + u2 * (320.0 - 175.0 * u2)));
let big_b =
u2 / 1024.0 * (256.0 + u2 * (-128.0 + u2 * (74.0 - 47.0 * u2)));
let mut sigma = distance / (WGS84_B * big_a);
let mut sigma_prev;
let mut iter = 0_u32;
let mut cos2_sigma_m;
let mut sin_sigma;
let mut cos_sigma;
loop {
cos2_sigma_m = (2.0 * sigma1 + sigma).cos();
sin_sigma = sigma.sin();
cos_sigma = sigma.cos();
let delta_sigma = big_b
* sin_sigma
* (cos2_sigma_m
+ big_b / 4.0
* (cos_sigma * (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)
- big_b / 6.0
* cos2_sigma_m
* (-3.0 + 4.0 * sin_sigma * sin_sigma)
* (-3.0 + 4.0 * cos2_sigma_m * cos2_sigma_m)));
sigma_prev = sigma;
sigma = distance / (WGS84_B * big_a) + delta_sigma;
iter += 1;
if (sigma - sigma_prev).abs() < TOLERANCE {
break;
}
if iter >= MAX_ITER {
return Err(GeoError::VincentyNonConvergence {
max_iter: MAX_ITER,
delta: (sigma - sigma_prev).abs(),
});
}
}
cos2_sigma_m = (2.0 * sigma1 + sigma).cos();
sin_sigma = sigma.sin();
cos_sigma = sigma.cos();
let lat2_rad = (sin_u1 * cos_sigma + cos_u1 * sin_sigma * cos_alpha1).atan2(
(1.0 - WGS84_F)
* (sin_alpha.powi(2)
+ (sin_u1 * sin_sigma - cos_u1 * cos_sigma * cos_alpha1).powi(2))
.sqrt(),
);
let lambda =
(sin_sigma * sin_alpha1).atan2(cos_u1 * cos_sigma - sin_u1 * sin_sigma * cos_alpha1);
let c = WGS84_F / 16.0
* cos2_alpha
* (4.0 + WGS84_F * (4.0 - 3.0 * cos2_alpha));
let l = lambda
- (1.0 - c)
* WGS84_F
* sin_alpha
* (sigma
+ c * sin_sigma
* (cos2_sigma_m
+ c * cos_sigma
* (-1.0 + 2.0 * cos2_sigma_m * cos2_sigma_m)));
let lon2 = deg2rad(self.lon) + l;
Ok(GeographicCoord {
lat: rad2deg(lat2_rad),
lon: rad2deg(lon2),
alt: self.alt, })
}
}
impl EcefCoord {
#[inline]
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
pub fn to_geographic(&self) -> GeographicCoord {
let p = (self.x * self.x + self.y * self.y).sqrt();
let lon = self.y.atan2(self.x);
let mut lat = (self.z / (p * (1.0 - WGS84_E2))).atan();
for _ in 0..10 {
let sin_lat = lat.sin();
let n = prime_vertical_radius(sin_lat);
let lat_new = (self.z + WGS84_E2 * n * sin_lat).atan2(p);
if (lat_new - lat).abs() < 1e-14 {
lat = lat_new;
break;
}
lat = lat_new;
}
let sin_lat = lat.sin();
let n = prime_vertical_radius(sin_lat);
let alt = p / lat.cos() - n;
GeographicCoord {
lat: rad2deg(lat),
lon: rad2deg(lon),
alt,
}
}
pub fn to_enu(&self, origin: &EcefCoord) -> EnuCoord {
let origin_geo = origin.to_geographic();
let lat = deg2rad(origin_geo.lat);
let lon = deg2rad(origin_geo.lon);
let dx = self.x - origin.x;
let dy = self.y - origin.y;
let dz = self.z - origin.z;
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let sin_lon = lon.sin();
let cos_lon = lon.cos();
EnuCoord {
east: -sin_lon * dx + cos_lon * dy,
north: -sin_lat * cos_lon * dx - sin_lat * sin_lon * dy + cos_lat * dz,
up: cos_lat * cos_lon * dx + cos_lat * sin_lon * dy + sin_lat * dz,
}
}
}
impl EnuCoord {
#[inline]
pub fn new(east: f64, north: f64, up: f64) -> Self {
Self { east, north, up }
}
#[inline]
pub fn horizontal_distance(&self) -> f64 {
(self.east * self.east + self.north * self.north).sqrt()
}
#[inline]
pub fn distance(&self) -> f64 {
(self.east * self.east + self.north * self.north + self.up * self.up).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
const DIST_TOL: f64 = 1.0;
const ANG_TOL: f64 = 1e-9;
#[test]
fn test_ecef_round_trip_london() {
let london = GeographicCoord::new(51.5074, -0.1278, 0.0);
let ecef = london.to_ecef();
let recovered = ecef.to_geographic();
assert!(
(recovered.lat - london.lat).abs() < ANG_TOL,
"lat error: {}",
(recovered.lat - london.lat).abs()
);
assert!(
(recovered.lon - london.lon).abs() < ANG_TOL,
"lon error: {}",
(recovered.lon - london.lon).abs()
);
assert!(
(recovered.alt - london.alt).abs() < 1e-4,
"alt error: {}",
(recovered.alt - london.alt).abs()
);
}
#[test]
fn test_ecef_round_trip_with_altitude() {
let coord = GeographicCoord::new(-33.8688, 151.2093, 500.0); let recovered = coord.to_ecef().to_geographic();
assert!((recovered.lat - coord.lat).abs() < ANG_TOL);
assert!((recovered.lon - coord.lon).abs() < ANG_TOL);
assert!((recovered.alt - coord.alt).abs() < 1e-4);
}
#[test]
fn test_ecef_round_trip_north_pole() {
let pole = GeographicCoord::new(89.9999, 0.0, 0.0);
let recovered = pole.to_ecef().to_geographic();
assert!((recovered.lat - pole.lat).abs() < 1e-6);
}
#[test]
fn test_haversine_london_paris() {
let london = GeographicCoord::new(51.5074, -0.1278, 0.0);
let paris = GeographicCoord::new(48.8566, 2.3522, 0.0);
let dist = london.haversine_distance(&paris);
assert!(
(dist - 340_000.0).abs() < 5_000.0,
"Haversine London–Paris: {dist:.0} m, expected ~340 000 m"
);
}
#[test]
fn test_haversine_same_point() {
let p = GeographicCoord::new(45.0, 90.0, 0.0);
let dist = p.haversine_distance(&p);
assert!(dist.abs() < 1e-6, "distance to self must be ~0, got {dist}");
}
#[test]
fn test_haversine_equatorial() {
let a = GeographicCoord::new(0.0, 0.0, 0.0);
let b = GeographicCoord::new(0.0, 1.0, 0.0);
let dist = a.haversine_distance(&b);
assert!((dist - 111_319.49).abs() < 1.0, "equatorial 1°: {dist:.2} m");
}
#[test]
fn test_vincenty_london_paris() {
let london = GeographicCoord::new(51.5074, -0.1278, 0.0);
let paris = GeographicCoord::new(48.8566, 2.3522, 0.0);
let dist = london.vincenty_distance(&paris).expect("vincenty should converge");
assert!(
(dist - 340_000.0).abs() < 5_000.0,
"Vincenty London–Paris: {dist:.0} m"
);
}
#[test]
fn test_vincenty_more_accurate_than_haversine() {
let nyc = GeographicCoord::new(40.7128, -74.0060, 0.0);
let london = GeographicCoord::new(51.5074, -0.1278, 0.0);
let hav = nyc.haversine_distance(&london);
let vin = nyc.vincenty_distance(&london).expect("vincenty convergence");
let rel_diff = (hav - vin).abs() / vin;
assert!(
rel_diff < 0.005,
"Haversine vs Vincenty relative diff: {rel_diff:.4} > 0.5%"
);
}
#[test]
fn test_vincenty_same_point() {
let p = GeographicCoord::new(45.0, 90.0, 0.0);
let dist = p.vincenty_distance(&p).expect("should converge");
assert!(dist.abs() < 1e-3, "distance to self must be ~0, got {dist}");
}
#[test]
fn test_bearing_to_north() {
let a = GeographicCoord::new(0.0, 0.0, 0.0);
let b = GeographicCoord::new(10.0, 0.0, 0.0);
let bearing = a.bearing_to(&b);
assert!(
bearing < 0.001 || (bearing - 360.0).abs() < 0.001,
"north bearing should be ~0, got {bearing}"
);
}
#[test]
fn test_bearing_to_east() {
let a = GeographicCoord::new(0.0, 0.0, 0.0);
let b = GeographicCoord::new(0.0, 10.0, 0.0);
let bearing = a.bearing_to(&b);
assert!(
(bearing - 90.0).abs() < 0.001,
"east bearing should be ~90, got {bearing}"
);
}
#[test]
fn test_bearing_in_range() {
let a = GeographicCoord::new(51.5074, -0.1278, 0.0);
let b = GeographicCoord::new(48.8566, 2.3522, 0.0);
let bearing = a.bearing_to(&b);
assert!((0.0..360.0).contains(&bearing), "bearing out of range: {bearing}");
}
#[test]
fn test_destination_round_trip() {
let start = GeographicCoord::new(51.5074, -0.1278, 0.0);
let other = GeographicCoord::new(48.8566, 2.3522, 0.0);
let dist = start.vincenty_distance(&other).expect("dist converges");
let bearing = start.vincenty_bearing(&other).expect("bearing converges");
let dest = start.destination(dist, bearing).expect("destination converges");
assert!(
(dest.lat - other.lat).abs() < 1e-5,
"lat error: {}",
(dest.lat - other.lat).abs()
);
assert!(
(dest.lon - other.lon).abs() < 1e-5,
"lon error: {}",
(dest.lon - other.lon).abs()
);
}
#[test]
fn test_destination_negative_distance_error() {
let p = GeographicCoord::new(0.0, 0.0, 0.0);
let result = p.destination(-1.0, 0.0);
assert!(result.is_err());
}
#[test]
fn test_enu_origin_maps_to_zero() {
let origin = GeographicCoord::new(51.5074, -0.1278, 0.0);
let enu = origin.to_enu(&origin);
assert!(enu.east.abs() < 1e-6, "east should be ~0: {}", enu.east);
assert!(enu.north.abs() < 1e-6, "north should be ~0: {}", enu.north);
assert!(enu.up.abs() < 1e-6, "up should be ~0: {}", enu.up);
}
#[test]
fn test_enu_north_offset() {
let origin = GeographicCoord::new(0.0, 0.0, 0.0);
let north_pt = GeographicCoord::new(1.0, 0.0, 0.0);
let enu = north_pt.to_enu(&origin);
assert!(
enu.north > 100_000.0,
"north component too small: {}",
enu.north
);
assert!(
enu.east.abs() < 1_000.0,
"east component too large: {}",
enu.east
);
}
#[test]
fn test_enu_horizontal_distance() {
let origin = GeographicCoord::new(0.0, 0.0, 0.0);
let pt = GeographicCoord::new(0.0, 1.0, 0.0);
let enu = pt.to_enu(&origin);
let h = enu.horizontal_distance();
assert!((h - 111_319.49).abs() < DIST_TOL * 10.0, "h = {h}");
}
}