use crate::morton::MortonCode;
pub const EARTH_RADIUS_IN_METERS: f64 = 6371000.8;
pub const LON_NDS_DELTA: f64 = 360.0 / (4294967295.0); pub const LAT_NDS_DELTA: f64 = 180.0 / (2147483647.0);
pub const LON_NDS_DELTA_POW2: f64 = 360.0 / 4294967296.0; pub const LAT_NDS_DELTA_POW2: f64 = 180.0 / 2147483648.0;
pub const LON_MIN: f64 = -180.0;
pub const LON_MAX: f64 = 180.0 - LON_NDS_DELTA_POW2;
pub const LAT_MIN: f64 = -90.0;
pub const LAT_MAX: f64 = 90.0 - LAT_NDS_DELTA_POW2;
pub const METERS_PER_DEGREE: f64 = 111320.0;
#[derive(Debug, Clone, Copy)]
pub struct Wgs84 {
pub lon: f64,
pub lat: f64,
pub alt: f64,
}
impl Wgs84 {
pub fn new(lon: f64, lat: f64) -> Self {
Self::with_alt(lon, lat, 0.0)
}
pub fn with_alt(lon: f64, lat: f64, alt: f64) -> Self {
let mut p = Wgs84 { lon, lat, alt };
p.normalize();
p
}
pub fn normalize(&mut self) {
self.lon %= 360.0;
if (self.lon - (180.0 - LON_NDS_DELTA)).abs() < LON_NDS_DELTA {
self.lon = 180.0 - LON_NDS_DELTA;
}
if self.lon >= 180.0 {
self.lon -= 360.0;
} else if self.lon < -180.0 {
self.lon += 360.0;
}
if (self.lat - (90.0 - LAT_NDS_DELTA)).abs() < LAT_NDS_DELTA {
self.lat = 90.0 - LAT_NDS_DELTA;
}
self.lat = self.lat.clamp(-90.0, 90.0 - LAT_NDS_DELTA);
}
pub fn to_nds_coordinates(&self) -> (i32, i32) {
let x_nds = ((self.lon / 360.0) * 4294967296.0).floor();
let y_nds = ((self.lat / 180.0) * 2147483648.0).floor();
(x_nds as i32, y_nds as i32)
}
pub fn from_nds_coordinates(x: i32, y: i32) -> Wgs84 {
let lon_multiplier = 360.0 / 4294967296.0; let lat_multiplier = 180.0 / 2147483648.0; let lon = (x as f64) * lon_multiplier;
let lat = (y as f64) * lat_multiplier;
Wgs84::new(lon, lat)
}
pub fn from_morton_code(morton_code: MortonCode) -> Wgs84 {
let bit_scaling = 360.0 / 4294967296.0; let (x, y) = morton_code.to_nds_coordinates();
Wgs84::new((x as f64) * bit_scaling, (y as f64) * bit_scaling)
}
pub fn degrees_to_meters(lon_degrees: f64, lat_degrees: f64, at_latitude: f64) -> (f64, f64) {
let lon_meters = lon_degrees.abs() * METERS_PER_DEGREE * at_latitude.to_radians().cos();
let lat_meters = lat_degrees.abs() * METERS_PER_DEGREE;
(lon_meters, lat_meters)
}
pub fn nds_distance_to_meters(
nds_x_distance: i32,
nds_y_distance: i32,
at_latitude: f64,
) -> (f64, f64) {
let lon_degrees = ((nds_x_distance as f64) / 4294967296.0) * 360.0; let lat_degrees = ((nds_y_distance as f64) / 2147483648.0) * 180.0; Wgs84::degrees_to_meters(lon_degrees, lat_degrees, at_latitude)
}
pub fn to_degree_minutes_seconds(&self) -> (String, String) {
fn convert(value: f64) -> String {
let degrees = value.trunc() as i64;
let minutes = ((value - degrees as f64) * 60.0).trunc() as i64;
let seconds = (value - degrees as f64 - (minutes as f64) / 60.0) * 3600.0;
format!("{}\u{00b0} {}' {:.2}\"", degrees, minutes, seconds)
}
let lat = convert(self.lat.abs()) + if self.lat < 0.0 { " S" } else { " N" };
let lon = convert(self.lon.abs()) + if self.lon < 0.0 { " W" } else { " E" };
(lat, lon)
}
pub fn distance_to(&self, other: &Wgs84) -> f64 {
let dlat = (other.lat - self.lat).to_radians();
let dlon = (other.lon - self.lon).to_radians();
let a = (dlat / 2.0).sin().powi(2)
+ self.lat.to_radians().cos()
* other.lat.to_radians().cos()
* (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_IN_METERS * c
}
pub fn bearing_from(&self, other: &Wgs84) -> f64 {
let lat1 = self.lat.to_radians();
let lon1 = self.lon.to_radians();
let lat2 = other.lat.to_radians();
let lon2 = other.lon.to_radians();
let y = (lon2 - lon1).sin() * lat2.cos();
let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * (lon2 - lon1).cos();
y.atan2(x)
}
}
impl PartialEq for Wgs84 {
fn eq(&self, other: &Self) -> bool {
is_close_rel(self.lon, other.lon, 1e-12) && is_close_rel(self.lat, other.lat, 1e-12)
}
}
fn is_close_rel(a: f64, b: f64, rel_tol: f64) -> bool {
(a - b).abs() <= rel_tol * a.abs().max(b.abs())
}
impl std::ops::Add for Wgs84 {
type Output = Wgs84;
fn add(self, other: Wgs84) -> Wgs84 {
Wgs84::new(self.lon + other.lon, self.lat + other.lat)
}
}
impl std::ops::Sub for Wgs84 {
type Output = Wgs84;
fn sub(self, other: Wgs84) -> Wgs84 {
Wgs84::new(self.lon - other.lon, self.lat - other.lat)
}
}
impl std::ops::Mul for Wgs84 {
type Output = Wgs84;
fn mul(self, other: Wgs84) -> Wgs84 {
Wgs84::new(self.lon * other.lon, self.lat * other.lat)
}
}
impl std::ops::Div for Wgs84 {
type Output = Wgs84;
fn div(self, other: Wgs84) -> Wgs84 {
Wgs84::new(self.lon / other.lon, self.lat / other.lat)
}
}
impl std::fmt::Display for Wgs84 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wgs84(lon={}, lat={})", self.lon, self.lat)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn origin_round_trip() {
let p = Wgs84::new(0.0, 0.0);
assert_eq!(p.to_nds_coordinates(), (0, 0));
}
#[test]
fn floor_not_truncation_for_negatives() {
let p = Wgs84::new(-1e-06, -1e-06);
assert_eq!(p.to_nds_coordinates(), (-12, -12));
}
#[test]
fn longitude_wrap() {
let p = Wgs84::new(360.5, 0.0);
assert!((p.lon - 0.5).abs() < 1e-9);
let n = Wgs84::new(-360.5, 0.0);
assert!((n.lon - (-0.5)).abs() < 1e-9);
}
#[test]
fn snap_180() {
let p = Wgs84::new(180.0, 90.0);
assert!((p.lon - (180.0 - LON_NDS_DELTA)).abs() < 1e-12);
assert!((p.lat - (90.0 - LAT_NDS_DELTA)).abs() < 1e-12);
assert_eq!(p.to_nds_coordinates(), (2147483647, 1073741823));
}
#[test]
fn min_corner() {
let p = Wgs84::new(-180.0, -90.0);
assert!((p.lon - (-180.0)).abs() < 1e-12);
assert!((p.lat - (-90.0)).abs() < 1e-12);
assert_eq!(p.to_nds_coordinates(), (-2147483648, -1073741824));
}
#[test]
fn dms_format() {
let p = Wgs84::new(13.404954, 52.520008);
let (lat, lon) = p.to_degree_minutes_seconds();
assert!(lat.ends_with(" N"));
assert!(lon.ends_with(" E"));
}
#[test]
fn geometry_constants() {
assert!((LON_NDS_DELTA_POW2 - 8.381903171539307e-08).abs() < 1e-20);
assert!((LAT_NDS_DELTA_POW2 - 8.381903171539307e-08).abs() < 1e-20);
assert_eq!(LON_MIN, -180.0);
assert!((LON_MAX - 179.99999991618097).abs() < 1e-9);
assert_eq!(LAT_MIN, -90.0);
assert_eq!(LON_MAX + LON_NDS_DELTA_POW2, 180.0);
}
#[test]
fn from_morton_code_scales_both_axes_by_pow2() {
let m = MortonCode::from_nds_coordinates(0, 0);
let p = Wgs84::from_morton_code(m);
assert_eq!((p.lon, p.lat), (0.0, 0.0));
let m2 = MortonCode::from_nds_coordinates(1 << 20, 1 << 20);
let p2 = Wgs84::from_morton_code(m2);
let expected = (1u64 << 20) as f64 * (360.0 / 4294967296.0);
assert!((p2.lon - expected).abs() < 1e-12);
assert!((p2.lat - expected).abs() < 1e-12);
}
#[test]
fn component_wise_arithmetic() {
let a = Wgs84::new(10.0, 20.0);
let b = Wgs84::new(3.0, 4.0);
assert_eq!(a + b, Wgs84::new(13.0, 24.0));
assert_eq!(a - b, Wgs84::new(7.0, 16.0));
assert_eq!(a * b, Wgs84::new(30.0, 80.0));
assert_eq!(a / b, Wgs84::new(10.0 / 3.0, 5.0));
}
}