use super::coordinates::{to_rad, EARTH_RADIUS_M, WGS84_A, WGS84_B, WGS84_F};
use crate::error::{SpatialError, SpatialResult};
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let dlat = to_rad(lat2 - lat1);
let dlon = to_rad(lon2 - lon1);
let lat1_rad = to_rad(lat1);
let lat2_rad = to_rad(lat2);
let a =
(dlat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
EARTH_RADIUS_M * c
}
pub fn vincenty_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> SpatialResult<f64> {
let lat1_rad = to_rad(lat1);
let lat2_rad = to_rad(lat2);
let lon1_rad = to_rad(lon1);
let lon2_rad = to_rad(lon2);
let f = WGS84_F;
let a = WGS84_A;
let b = WGS84_B;
let l = lon2_rad - lon1_rad;
let u1 = ((1.0 - f) * lat1_rad.tan()).atan();
let u2 = ((1.0 - f) * lat2_rad.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;
'convergence: {
for _ in 0..1000 {
let sin_lambda = lambda.sin();
let cos_lambda = lambda.cos();
let sin_sigma_sq = (cos_u2 * sin_lambda).powi(2)
+ (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2);
let sin_sigma = sin_sigma_sq.sqrt();
if sin_sigma.abs() < 1e-12 {
break 'convergence Ok(0.0);
}
let cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
let sigma = sin_sigma.atan2(cos_sigma);
let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma;
let cos_sq_alpha = 1.0 - sin_alpha.powi(2);
let cos_2sigma_m = if cos_sq_alpha.abs() < 1e-12 {
0.0 } else {
cos_sigma - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha
};
let c = f / 16.0 * cos_sq_alpha * (4.0 + f * (4.0 - 3.0 * cos_sq_alpha));
let lambda_new = l
+ (1.0 - c)
* f
* sin_alpha
* (sigma
+ c * sin_sigma
* (cos_2sigma_m + c * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))));
if (lambda_new - lambda).abs() < 1e-12 {
let u_sq = cos_sq_alpha * (a * a - b * b) / (b * b);
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 delta_sigma = big_b
* sin_sigma
* (cos_2sigma_m
+ big_b / 4.0
* (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))
- big_b / 6.0
* cos_2sigma_m
* (-3.0 + 4.0 * sin_sigma.powi(2))
* (-3.0 + 4.0 * cos_2sigma_m.powi(2))));
break 'convergence Ok(b * big_a * (sigma - delta_sigma));
}
lambda = lambda_new;
}
Err(SpatialError::ComputationError(
"Vincenty formula did not converge (nearly antipodal points?)".to_string(),
))
}
}
pub fn bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let lat1_rad = to_rad(lat1);
let lat2_rad = to_rad(lat2);
let dlon = to_rad(lon2 - lon1);
let y = dlon.sin() * lat2_rad.cos();
let x = lat1_rad.cos() * lat2_rad.sin() - lat1_rad.sin() * lat2_rad.cos() * dlon.cos();
let theta = y.atan2(x).to_degrees();
(theta + 360.0) % 360.0
}
pub fn destination_point(lat: f64, lon: f64, bearing_deg: f64, distance: f64) -> (f64, f64) {
let lat_rad = to_rad(lat);
let lon_rad = to_rad(lon);
let bearing_rad = to_rad(bearing_deg);
let angular_dist = distance / EARTH_RADIUS_M;
let lat2_rad = (lat_rad.sin() * angular_dist.cos()
+ lat_rad.cos() * angular_dist.sin() * bearing_rad.cos())
.asin();
let lon2_rad = lon_rad
+ (bearing_rad.sin() * angular_dist.sin() * lat_rad.cos())
.atan2(angular_dist.cos() - lat_rad.sin() * lat2_rad.sin());
(lat2_rad.to_degrees(), lon2_rad.to_degrees())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_haversine_same_point() {
assert!(haversine_distance(51.5074, -0.1278, 51.5074, -0.1278) < 1e-6);
}
#[test]
fn test_haversine_london_paris() {
let dist = haversine_distance(51.5074, -0.1278, 48.8566, 2.3522);
assert!((dist - 343_000.0).abs() < 5_000.0, "dist={dist}");
}
#[test]
fn test_haversine_ny_london() {
let dist = haversine_distance(40.7128, -74.0060, 51.5074, -0.1278);
assert!((dist - 5_570_000.0).abs() < 50_000.0, "dist={dist}");
}
#[test]
fn test_vincenty_vs_haversine_short_distance() {
let h = haversine_distance(51.5074, -0.1278, 48.8566, 2.3522);
let v = vincenty_distance(51.5074, -0.1278, 48.8566, 2.3522).expect("vincenty");
let diff_pct = (h - v).abs() / v * 100.0;
assert!(diff_pct < 0.5, "diff={diff_pct}%");
}
#[test]
fn test_vincenty_same_point() {
let d = vincenty_distance(0.0, 0.0, 0.0, 0.0).expect("vincenty");
assert!(d < 1e-6, "d={d}");
}
#[test]
fn test_bearing_north() {
let b = bearing(0.0, 0.0, 1.0, 0.0);
assert!(b < 1e-6 || (b - 360.0).abs() < 1e-6, "b={b}");
}
#[test]
fn test_bearing_east() {
let b = bearing(0.0, 0.0, 0.0, 1.0);
assert!((b - 90.0).abs() < 0.01, "b={b}");
}
#[test]
fn test_bearing_south() {
let b = bearing(1.0, 0.0, 0.0, 0.0);
assert!((b - 180.0).abs() < 0.01, "b={b}");
}
#[test]
fn test_bearing_west() {
let b = bearing(0.0, 1.0, 0.0, 0.0);
assert!((b - 270.0).abs() < 0.01, "b={b}");
}
#[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}");
let back = haversine_distance(0.0, 0.0, lat2, lon2);
assert!((back - 100_000.0).abs() < 100.0, "back={back}");
}
#[test]
fn test_destination_roundtrip() {
let (lat2, lon2) = destination_point(40.0, -74.0, 45.0, 500_000.0);
let b = bearing(40.0, -74.0, lat2, lon2);
assert!((b - 45.0).abs() < 0.1, "bearing={b}");
let d = haversine_distance(40.0, -74.0, lat2, lon2);
assert!((d - 500_000.0).abs() < 1000.0, "dist={d}");
}
}