use crate::error::{SpatialError, SpatialResult};
use std::f64::consts::PI;
pub const EARTH_RADIUS_M: f64 = 6_371_008.8;
pub const EARTH_RADIUS_KM: f64 = EARTH_RADIUS_M / 1000.0;
pub const EARTH_EQUATORIAL_RADIUS_M: f64 = 6_378_137.0;
pub const EARTH_POLAR_RADIUS_M: f64 = 6_356_752.314245;
pub const EARTH_FLATTENING: f64 = 1.0 / 298.257223563;
pub const EARTH_ECCENTRICITY_SQ: f64 = 2.0 * EARTH_FLATTENING - EARTH_FLATTENING * EARTH_FLATTENING;
#[allow(dead_code)]
pub fn deg_to_rad(degrees: f64) -> f64 {
degrees * PI / 180.0
}
#[allow(dead_code)]
pub fn rad_to_deg(radians: f64) -> f64 {
radians * 180.0 / PI
}
#[allow(dead_code)]
pub fn normalize_angle(angle: f64) -> f64 {
let normalized = angle % (2.0 * PI);
if normalized < 0.0 {
normalized + 2.0 * PI
} else {
normalized
}
}
#[allow(dead_code)]
pub fn normalize_bearing(_bearingdeg: f64) -> f64 {
let normalized = _bearingdeg % 360.0;
if normalized < 0.0 {
normalized + 360.0
} else {
normalized
}
}
#[allow(dead_code)]
pub fn haversine_distance(point1: (f64, f64), point2: (f64, f64)) -> f64 {
let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
let dlat = lat2 - lat1;
let dlon = lon2 - lon1;
let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
EARTH_RADIUS_M * c
}
#[allow(dead_code)]
pub fn initial_bearing(point1: (f64, f64), point2: (f64, f64)) -> f64 {
let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
let dlon = lon2 - lon1;
let y = dlon.sin() * lat2.cos();
let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
normalize_angle(y.atan2(x))
}
#[allow(dead_code)]
pub fn final_bearing(point1: (f64, f64), point2: (f64, f64)) -> f64 {
let reverse_bearing = initial_bearing(point2, point1);
normalize_angle(reverse_bearing + PI)
}
#[allow(dead_code)]
pub fn destination_point(start: (f64, f64), distance: f64, bearing: f64) -> (f64, f64) {
let (lat1, lon1) = (deg_to_rad(start.0), deg_to_rad(start.1));
let angular_distance = distance / EARTH_RADIUS_M;
let lat2 = (lat1.sin() * angular_distance.cos()
+ lat1.cos() * angular_distance.sin() * bearing.cos())
.asin();
let lon2 = lon1
+ (bearing.sin() * angular_distance.sin() * lat1.cos())
.atan2(angular_distance.cos() - lat1.sin() * lat2.sin());
(rad_to_deg(lat2), rad_to_deg(lon2))
}
#[allow(dead_code)]
pub fn midpoint(point1: (f64, f64), point2: (f64, f64)) -> (f64, f64) {
let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
let dlon = lon2 - lon1;
let bx = lat2.cos() * dlon.cos();
let by = lat2.cos() * dlon.sin();
let lat_mid = (lat1.sin() + lat2.sin()).atan2(((lat1.cos() + bx).powi(2) + by.powi(2)).sqrt());
let lon_mid = lon1 + by.atan2(lat1.cos() + bx);
(rad_to_deg(lat_mid), rad_to_deg(lon_mid))
}
#[allow(dead_code)]
pub fn cross_track_distance(
point: (f64, f64),
path_start: (f64, f64),
path_end: (f64, f64),
) -> f64 {
let distance_to_start = haversine_distance(path_start, point) / EARTH_RADIUS_M;
let bearing_to_point = initial_bearing(path_start, point);
let bearing_to_end = initial_bearing(path_start, path_end);
let cross_track_angular =
(distance_to_start.sin() * (bearing_to_point - bearing_to_end).sin()).asin();
EARTH_RADIUS_M * cross_track_angular
}
#[allow(dead_code)]
pub fn along_track_distance(
point: (f64, f64),
path_start: (f64, f64),
path_end: (f64, f64),
) -> f64 {
let distance_to_start = haversine_distance(path_start, point) / EARTH_RADIUS_M;
let cross_track_angular = cross_track_distance(point, path_start, path_end) / EARTH_RADIUS_M;
let along_track_angular = (distance_to_start.powi(2) - cross_track_angular.powi(2))
.sqrt()
.acos();
EARTH_RADIUS_M * along_track_angular
}
#[allow(dead_code)]
pub fn spherical_polygon_area(polygon: &[(f64, f64)]) -> SpatialResult<f64> {
if polygon.len() < 3 {
return Err(SpatialError::ValueError(
"Polygon must have at least 3 vertices".to_string(),
));
}
let n = polygon.len();
let mut sum = 0.0;
for i in 0..n {
let j = (i + 1) % n;
let (lat1, lon1) = (deg_to_rad(polygon[i].0), deg_to_rad(polygon[i].1));
let (lat2, lon2) = (deg_to_rad(polygon[j].0), deg_to_rad(polygon[j].1));
sum += (lon2 - lon1) * (2.0 + lat1.sin() + lat2.sin());
}
let area = (sum.abs() / 2.0) * EARTH_RADIUS_M * EARTH_RADIUS_M;
Ok(area)
}
#[allow(dead_code)]
pub fn point_in_spherical_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
if polygon.len() < 3 {
return false;
}
let max_extent = polygon
.iter()
.flat_map(|(lat, lon)| [lat.abs(), lon.abs()])
.fold(0.0, f64::max);
if max_extent < 10.0 {
let (x, y) = point;
let mut inside = false;
let n = polygon.len();
for i in 0..n {
let j = (i + 1) % n;
let (xi, yi) = polygon[i];
let (xj, yj) = polygon[j];
if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
inside = !inside;
}
}
return inside;
}
let (test_lat, test_lon) = (deg_to_rad(point.0), deg_to_rad(point.1));
let mut angle_sum = 0.0;
for i in 0..polygon.len() {
let j = (i + 1) % polygon.len();
let (lat1, lon1) = (deg_to_rad(polygon[i].0), deg_to_rad(polygon[i].1));
let (lat2, lon2) = (deg_to_rad(polygon[j].0), deg_to_rad(polygon[j].1));
let x1 = lat1.cos() * lon1.cos();
let y1 = lat1.cos() * lon1.sin();
let z1 = lat1.sin();
let x2 = lat2.cos() * lon2.cos();
let y2 = lat2.cos() * lon2.sin();
let z2 = lat2.sin();
let xt = test_lat.cos() * test_lon.cos();
let yt = test_lat.cos() * test_lon.sin();
let zt = test_lat.sin();
let v1x = x1 - xt;
let v1y = y1 - yt;
let v1z = z1 - zt;
let v2x = x2 - xt;
let v2y = y2 - yt;
let v2z = z2 - zt;
let v1_len = (v1x * v1x + v1y * v1y + v1z * v1z).sqrt();
let v2_len = (v2x * v2x + v2y * v2y + v2z * v2z).sqrt();
if v1_len < 1e-10 || v2_len < 1e-10 {
continue; }
let v1x_norm = v1x / v1_len;
let v1y_norm = v1y / v1_len;
let v1z_norm = v1z / v1_len;
let v2x_norm = v2x / v2_len;
let v2y_norm = v2y / v2_len;
let v2z_norm = v2z / v2_len;
let dot = v1x_norm * v2x_norm + v1y_norm * v2y_norm + v1z_norm * v2z_norm;
let dot = dot.clamp(-1.0, 1.0);
let cross_x = v1y_norm * v2z_norm - v1z_norm * v2y_norm;
let cross_y = v1z_norm * v2x_norm - v1x_norm * v2z_norm;
let cross_z = v1x_norm * v2y_norm - v1y_norm * v2x_norm;
let normal_dot = cross_x * xt + cross_y * yt + cross_z * zt;
let angle = dot.acos();
if normal_dot < 0.0 {
angle_sum -= angle;
} else {
angle_sum += angle;
}
}
(angle_sum.abs() / (2.0 * PI)) > 0.5
}
#[allow(dead_code)]
pub fn geographic_to_utm(lat: f64, lon: f64) -> SpatialResult<(f64, f64, i32, char)> {
if !(-80.0..=84.0).contains(&lat) {
return Err(SpatialError::ValueError(
"Latitude must be between -80° and 84° for UTM".to_string(),
));
}
let zone_number = ((lon + 180.0) / 6.0).floor() as i32 + 1;
let zone_letter = utm_zone_letter(lat)?;
let lat_rad = deg_to_rad(lat);
let lon_rad = deg_to_rad(lon);
let central_meridian = deg_to_rad(((zone_number - 1) * 6 - 177) as f64);
let k0 = 0.9996; let a = EARTH_EQUATORIAL_RADIUS_M;
let e_sq = EARTH_ECCENTRICITY_SQ;
let n = a / (1.0 - e_sq * lat_rad.sin().powi(2)).sqrt();
let t = lat_rad.tan().powi(2);
let c = EARTH_ECCENTRICITY_SQ * lat_rad.cos().powi(2) / (1.0 - EARTH_ECCENTRICITY_SQ);
let a_coeff = lat_rad.cos() * (lon_rad - central_meridian);
let m = a
* ((1.0 - e_sq / 4.0 - 3.0 * e_sq.powi(2) / 64.0 - 5.0 * e_sq.powi(3) / 256.0) * lat_rad
- (3.0 * e_sq / 8.0 + 3.0 * e_sq.powi(2) / 32.0 + 45.0 * e_sq.powi(3) / 1024.0)
* (2.0 * lat_rad).sin()
+ (15.0 * e_sq.powi(2) / 256.0 + 45.0 * e_sq.powi(3) / 1024.0) * (4.0 * lat_rad).sin()
- (35.0 * e_sq.powi(3) / 3072.0) * (6.0 * lat_rad).sin());
let easting = k0
* n
* (a_coeff
+ (1.0 - t + c) * a_coeff.powi(3) / 6.0
+ (5.0 - 18.0 * t + t.powi(2) + 72.0 * c - 58.0 * EARTH_ECCENTRICITY_SQ)
* a_coeff.powi(5)
/ 120.0)
+ 500000.0;
let northing = k0
* (m + n
* lat_rad.tan()
* (a_coeff.powi(2) / 2.0
+ (5.0 - t + 9.0 * c + 4.0 * c.powi(2)) * a_coeff.powi(4) / 24.0
+ (61.0 - 58.0 * t + t.powi(2) + 600.0 * c - 330.0 * EARTH_ECCENTRICITY_SQ)
* a_coeff.powi(6)
/ 720.0));
let final_northing = if lat < 0.0 {
northing + 10000000.0
} else {
northing
};
Ok((easting, final_northing, zone_number, zone_letter))
}
#[allow(dead_code)]
fn utm_zone_letter(lat: f64) -> SpatialResult<char> {
let letters = [
'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X',
];
if !(-80.0..=84.0).contains(&lat) {
return Err(SpatialError::ValueError(
"Latitude out of UTM range".to_string(),
));
}
let index = ((lat + 80.0) / 8.0).floor() as usize;
if index < letters.len() {
Ok(letters[index])
} else {
Ok('X') }
}
#[allow(dead_code)]
pub fn geographic_to_web_mercator(lat: f64, lon: f64) -> SpatialResult<(f64, f64)> {
if lat.abs() >= 85.051_128_779_806_59 {
return Err(SpatialError::ValueError(
"Latitude must be between -85.051° and 85.051° for Web Mercator".to_string(),
));
}
let x = deg_to_rad(lon) * EARTH_EQUATORIAL_RADIUS_M;
let y = ((deg_to_rad(lat) / 2.0 + PI / 4.0).tan()).ln() * EARTH_EQUATORIAL_RADIUS_M;
Ok((x, y))
}
#[allow(dead_code)]
pub fn web_mercator_to_geographic(x: f64, y: f64) -> (f64, f64) {
let lon = rad_to_deg(x / EARTH_EQUATORIAL_RADIUS_M);
let lat = rad_to_deg(2.0 * ((y / EARTH_EQUATORIAL_RADIUS_M).exp().atan() - PI / 4.0));
(lat, lon)
}
#[allow(dead_code)]
pub fn vincenty_distance(point1: (f64, f64), point2: (f64, f64)) -> SpatialResult<f64> {
let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
let a = EARTH_EQUATORIAL_RADIUS_M;
let b = EARTH_POLAR_RADIUS_M;
let f = EARTH_FLATTENING;
let l = lon2 - lon1;
let u1 = ((1.0 - f) * lat1.tan()).atan();
let u2 = ((1.0 - 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 iteration_limit = 100;
let (cos_sq_alpha, sin_sigma, cos_sigma, sigma, cos_2sigma_m) = loop {
iteration_limit -= 1;
if iteration_limit == 0 {
return Err(SpatialError::ComputationError(
"Vincenty formula failed to converge".to_string(),
));
}
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();
if sin_sigma == 0.0 {
return 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 == 0.0 {
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));
lambda_prev = lambda;
lambda = 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 - lambda_prev).abs() < 1e-12 {
break (cos_sq_alpha, sin_sigma, cos_sigma, sigma, cos_2sigma_m);
}
};
let u_sq = cos_sq_alpha * (a.powi(2) - b.powi(2)) / 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))));
let distance = b * a_coeff * (sigma - delta_sigma);
Ok(distance)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_degree_radian_conversion() {
assert_relative_eq!(deg_to_rad(0.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(deg_to_rad(90.0), PI / 2.0, epsilon = 1e-10);
assert_relative_eq!(deg_to_rad(180.0), PI, epsilon = 1e-10);
assert_relative_eq!(deg_to_rad(360.0), 2.0 * PI, epsilon = 1e-10);
assert_relative_eq!(rad_to_deg(0.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(rad_to_deg(PI / 2.0), 90.0, epsilon = 1e-10);
assert_relative_eq!(rad_to_deg(PI), 180.0, epsilon = 1e-10);
assert_relative_eq!(rad_to_deg(2.0 * PI), 360.0, epsilon = 1e-10);
}
#[test]
fn test_haversine_distance() {
let london = (51.5074, -0.1278);
let paris = (48.8566, 2.3522);
let distance = haversine_distance(london, paris);
assert!((distance - 344_000.0).abs() < 5_000.0);
assert_relative_eq!(haversine_distance(london, london), 0.0, epsilon = 1e-6);
let north_pole = (90.0, 0.0);
let south_pole = (-90.0, 0.0);
let antipodal_distance = haversine_distance(north_pole, south_pole);
let expected_distance = PI * EARTH_RADIUS_M;
assert_relative_eq!(antipodal_distance, expected_distance, epsilon = 1000.0);
}
#[test]
fn test_initial_bearing() {
let london = (51.5074, -0.1278);
let paris = (48.8566, 2.3522);
let bearing = initial_bearing(london, paris);
let bearing_deg = rad_to_deg(bearing);
assert!(bearing_deg > 100.0 && bearing_deg < 180.0);
let start = (0.0, 0.0);
let north = (1.0, 0.0);
let north_bearing = initial_bearing(start, north);
assert_relative_eq!(north_bearing, 0.0, epsilon = 1e-6);
let east = (0.0, 1.0);
let east_bearing = initial_bearing(start, east);
assert_relative_eq!(east_bearing, PI / 2.0, epsilon = 1e-6);
}
#[test]
fn test_destination_point() {
let start = (51.5074, -0.1278); let distance = 100_000.0; let bearing = 0.0;
let destination = destination_point(start, distance, bearing);
assert!(destination.0 > start.0); assert!((destination.1 - start.1).abs() < 0.1);
let calculated_distance = haversine_distance(start, destination);
assert_relative_eq!(calculated_distance, distance, epsilon = 1000.0); }
#[test]
fn test_midpoint() {
let london = (51.5074, -0.1278);
let paris = (48.8566, 2.3522);
let mid = midpoint(london, paris);
assert!(mid.0 < london.0 && mid.0 > paris.0); assert!(mid.1 > london.1 && mid.1 < paris.1);
let dist_to_london = haversine_distance(mid, london);
let dist_to_paris = haversine_distance(mid, paris);
assert_relative_eq!(dist_to_london, dist_to_paris, epsilon = 1000.0);
}
#[test]
fn test_normalize_angle() {
assert_relative_eq!(normalize_angle(0.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(normalize_angle(PI), PI, epsilon = 1e-10);
assert_relative_eq!(normalize_angle(2.0 * PI), 0.0, epsilon = 1e-10);
assert_relative_eq!(normalize_angle(-PI), PI, epsilon = 1e-10);
assert_relative_eq!(normalize_angle(3.0 * PI), PI, epsilon = 1e-10);
}
#[test]
fn test_normalize_bearing() {
assert_relative_eq!(normalize_bearing(0.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(normalize_bearing(180.0), 180.0, epsilon = 1e-10);
assert_relative_eq!(normalize_bearing(360.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(normalize_bearing(-90.0), 270.0, epsilon = 1e-10);
assert_relative_eq!(normalize_bearing(450.0), 90.0, epsilon = 1e-10);
}
#[test]
fn test_spherical_polygon_area() {
let triangle = vec![
(0.0, 0.0), (0.0, 1.0), (1.0, 0.0), ];
let area = spherical_polygon_area(&triangle).expect("Operation failed");
assert!(area > 0.0);
let expected = (PI / 180.0).powi(2) * EARTH_RADIUS_M.powi(2) / 2.0;
assert_relative_eq!(area, expected, epsilon = expected * 0.1);
}
#[test]
fn test_geographic_to_web_mercator() {
let (x, y) = geographic_to_web_mercator(0.0, 0.0).expect("Operation failed");
assert_relative_eq!(x, 0.0, epsilon = 1e-6);
assert_relative_eq!(y, 0.0, epsilon = 1e-6);
let original = (45.0, -90.0);
let (x, y) = geographic_to_web_mercator(original.0, original.1).expect("Operation failed");
let back = web_mercator_to_geographic(x, y);
assert_relative_eq!(back.0, original.0, epsilon = 1e-6);
assert_relative_eq!(back.1, original.1, epsilon = 1e-6);
let result = geographic_to_web_mercator(86.0, 0.0);
assert!(result.is_err());
}
#[test]
fn test_geographic_to_utm() {
let london = (51.5074, -0.1278);
let (easting, northing, zone, letter) =
geographic_to_utm(london.0, london.1).expect("Operation failed");
assert!(zone == 30 || zone == 31);
assert!(letter == 'U' || letter == 'V');
assert!(easting > 400_000.0 && easting < 700_000.0);
assert!(northing > 5_700_000.0 && northing < 5_800_000.0);
assert!(geographic_to_utm(85.0, 0.0).is_err()); assert!(geographic_to_utm(-85.0, 0.0).is_err()); }
#[test]
fn test_cross_track_distance() {
let start = (51.0, 0.0);
let end = (52.0, 1.0);
let point = (51.5, 0.0);
let cross_track = cross_track_distance(point, start, end);
assert!(cross_track.abs() < 50_000.0); }
#[test]
fn test_vincenty_distance() {
let london = (51.5074, -0.1278);
let paris = (48.8566, 2.3522);
let haversine_dist = haversine_distance(london, paris);
let vincenty_dist = vincenty_distance(london, paris).expect("Operation failed");
let diff_percent = ((vincenty_dist - haversine_dist) / haversine_dist * 100.0).abs();
assert!(diff_percent < 1.0);
let same_point_dist = vincenty_distance(london, london).expect("Operation failed");
assert_relative_eq!(same_point_dist, 0.0, epsilon = 1e-6);
}
#[test]
fn test_point_in_spherical_polygon() {
let square = vec![(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)];
assert!(point_in_spherical_polygon((0.0, 0.0), &square));
assert!(!point_in_spherical_polygon((2.0, 2.0), &square));
let _ = point_in_spherical_polygon((1.0, 0.0), &square);
}
}