use std::f64::consts::PI;
const EARTH_RADIUS_KM: f64 = 6_371.0;
const EARTH_RADIUS_M: f64 = 6_371_000.0;
const WGS84_A: f64 = 6_378_137.0; const WGS84_F: f64 = 1.0 / 298.257_223_563; const WGS84_B: f64 = WGS84_A * (1.0 - WGS84_F);
#[inline]
fn to_rad(deg: f64) -> f64 {
deg * PI / 180.0
}
#[inline]
fn to_deg(rad: f64) -> f64 {
rad * 180.0 / PI
}
#[inline]
pub fn micro_to_deg(micro: i32) -> f64 {
micro as f64 / 1_000_000.0
}
#[inline]
pub fn deg_to_micro(deg: f64) -> i32 {
(deg * 1_000_000.0).round() as i32
}
pub fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let dlat = to_rad(lat2 - lat1);
let dlon = to_rad(lon2 - lon1);
let lat1_r = to_rad(lat1);
let lat2_r = to_rad(lat2);
let a = (dlat / 2.0).sin().powi(2) + lat1_r.cos() * lat2_r.cos() * (dlon / 2.0).sin().powi(2);
EARTH_RADIUS_KM * 2.0 * a.sqrt().asin()
}
pub fn haversine_m(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
haversine_km(lat1, lon1, lat2, lon2) * 1000.0
}
pub fn vincenty_m(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let u1 = ((1.0 - WGS84_F) * to_rad(lat1).tan()).atan();
let u2 = ((1.0 - WGS84_F) * to_rad(lat2).tan()).atan();
let l = to_rad(lon2 - lon1);
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 prev_lambda;
let mut sin_sigma;
let mut cos_sigma;
let mut sigma;
let mut sin_alpha;
let mut cos2_alpha;
let mut cos_2sigma_m;
for _ in 0..100 {
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();
if sin_sigma == 0.0 {
return 0.0; }
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;
cos2_alpha = 1.0 - sin_alpha.powi(2);
cos_2sigma_m = if cos2_alpha != 0.0 {
cos_sigma - 2.0 * sin_u1 * sin_u2 / cos2_alpha
} else {
0.0
};
let c = WGS84_F / 16.0 * cos2_alpha * (4.0 + WGS84_F * (4.0 - 3.0 * cos2_alpha));
prev_lambda = lambda;
lambda = 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.powi(2))));
if (lambda - prev_lambda).abs() < 1e-12 {
let u_sq = cos2_alpha * (WGS84_A.powi(2) - WGS84_B.powi(2)) / WGS84_B.powi(2);
let a_coeff =
1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
let b_coeff = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
let delta_sigma = b_coeff
* sin_sigma
* (cos_2sigma_m
+ b_coeff / 4.0
* (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))
- b_coeff / 6.0
* cos_2sigma_m
* (-3.0 + 4.0 * sin_sigma.powi(2))
* (-3.0 + 4.0 * cos_2sigma_m.powi(2))));
return WGS84_B * a_coeff * (sigma - delta_sigma);
}
}
haversine_m(lat1, lon1, lat2, lon2)
}
pub fn vincenty_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
vincenty_m(lat1, lon1, lat2, lon2) / 1000.0
}
pub fn bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let lat1_r = to_rad(lat1);
let lat2_r = to_rad(lat2);
let dlon = to_rad(lon2 - lon1);
let y = dlon.sin() * lat2_r.cos();
let x = lat1_r.cos() * lat2_r.sin() - lat1_r.sin() * lat2_r.cos() * dlon.cos();
(to_deg(y.atan2(x)) + 360.0) % 360.0
}
pub fn final_bearing(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
(bearing(lat2, lon2, lat1, lon1) + 180.0) % 360.0
}
pub fn midpoint(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> (f64, f64) {
let lat1_r = to_rad(lat1);
let lat2_r = to_rad(lat2);
let dlon = to_rad(lon2 - lon1);
let bx = lat2_r.cos() * dlon.cos();
let by = lat2_r.cos() * dlon.sin();
let lat =
(lat1_r.sin() + lat2_r.sin()).atan2(((lat1_r.cos() + bx).powi(2) + by.powi(2)).sqrt());
let lon = to_rad(lon1) + by.atan2(lat1_r.cos() + bx);
(to_deg(lat), to_deg(lon))
}
pub fn destination(lat: f64, lon: f64, bearing_deg: f64, distance_km: f64) -> (f64, f64) {
let lat_r = to_rad(lat);
let brng_r = to_rad(bearing_deg);
let d = distance_km / EARTH_RADIUS_KM;
let lat2 = (lat_r.sin() * d.cos() + lat_r.cos() * d.sin() * brng_r.cos()).asin();
let lon2 = to_rad(lon)
+ (brng_r.sin() * d.sin() * lat_r.cos()).atan2(d.cos() - lat_r.sin() * lat2.sin());
(to_deg(lat2), to_deg(lon2))
}
pub fn bounding_box(lat: f64, lon: f64, radius_km: f64) -> (f64, f64, f64, f64) {
let lat_delta = radius_km / 111.32;
let lon_delta = radius_km / (111.32 * to_rad(lat).cos().max(0.0001));
let min_lat = (lat - lat_delta).max(-90.0);
let max_lat = (lat + lat_delta).min(90.0);
let min_lon = lon - lon_delta;
let max_lon = lon + lon_delta;
(min_lat, min_lon, max_lat, max_lon)
}
pub fn polygon_area_km2(vertices: &[(f64, f64)]) -> f64 {
let n = vertices.len();
if n < 3 {
return 0.0;
}
let mut total = 0.0f64;
for i in 0..n {
let (lat1, lon1) = vertices[i];
let (lat2, lon2) = vertices[(i + 1) % n];
total += to_rad(lon2 - lon1) * (2.0 + to_rad(lat1).sin() + to_rad(lat2).sin());
}
(total.abs() / 2.0) * EARTH_RADIUS_KM * EARTH_RADIUS_KM
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_haversine_paris_london() {
let d = haversine_km(48.8566, 2.3522, 51.5074, -0.1278);
assert!((d - 344.0).abs() < 5.0, "Paris-London: {d} km");
}
#[test]
fn test_haversine_zero_distance() {
let d = haversine_km(0.0, 0.0, 0.0, 0.0);
assert!(d.abs() < 0.001, "same point: {d} km");
}
#[test]
fn test_haversine_antipodal() {
let d = haversine_km(0.0, 0.0, 0.0, 180.0);
assert!((d - 20015.0).abs() < 100.0, "antipodal: {d} km");
}
#[test]
fn test_vincenty_paris_london() {
let d = vincenty_km(48.8566, 2.3522, 51.5074, -0.1278);
assert!((d - 343.5).abs() < 2.0, "Vincenty Paris-London: {d} km");
}
#[test]
fn test_vincenty_coincident() {
let d = vincenty_m(48.8566, 2.3522, 48.8566, 2.3522);
assert!(d.abs() < 0.001, "coincident: {d} m");
}
#[test]
fn test_vincenty_new_york_tokyo() {
let d = vincenty_km(40.7128, -74.0060, 35.6762, 139.6503);
assert!((d - 10838.0).abs() < 50.0, "NY-Tokyo: {d} km");
}
#[test]
fn test_bearing_north() {
let b = bearing(0.0, 0.0, 1.0, 0.0);
assert!((b - 0.0).abs() < 1.0, "north bearing: {b}°");
}
#[test]
fn test_bearing_east() {
let b = bearing(0.0, 0.0, 0.0, 1.0);
assert!((b - 90.0).abs() < 1.0, "east bearing: {b}°");
}
#[test]
fn test_midpoint_equator() {
let (lat, lon) = midpoint(0.0, 0.0, 0.0, 10.0);
assert!((lat - 0.0).abs() < 0.01, "midpoint lat: {lat}");
assert!((lon - 5.0).abs() < 0.01, "midpoint lon: {lon}");
}
#[test]
fn test_destination() {
let (lat, lon) = destination(0.0, 0.0, 0.0, 111.32);
assert!((lat - 1.0).abs() < 0.1, "destination lat: {lat}");
assert!(lon.abs() < 0.1, "destination lon: {lon}");
}
#[test]
fn test_bounding_box() {
let (min_lat, min_lon, max_lat, max_lon) = bounding_box(0.0, 0.0, 111.32);
assert!((min_lat - (-1.0)).abs() < 0.1);
assert!((max_lat - 1.0).abs() < 0.1);
assert!(min_lon < 0.0);
assert!(max_lon > 0.0);
}
#[test]
fn test_micro_conversion() {
let lat = -23.550520;
let micro = deg_to_micro(lat);
let back = micro_to_deg(micro);
assert!((lat - back).abs() < 0.000001);
}
}