use crate::data::GeoEcefPoint;
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 = WGS84_F * (2.0 - WGS84_F);
pub const WGS84_E_PRIME_SQ: f64 = WGS84_E2 / (1.0 - WGS84_E2);
pub fn wgs84_to_ecef(lat_deg: f64, lon_deg: f64, height_m: f64) -> GeoEcefPoint {
let lat = lat_deg.to_radians();
let lon = lon_deg.to_radians();
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let sin_lon = lon.sin();
let cos_lon = lon.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let x = (n + height_m) * cos_lat * cos_lon;
let y = (n + height_m) * cos_lat * sin_lon;
let z = (n * (1.0 - WGS84_E2) + height_m) * sin_lat;
GeoEcefPoint::new(x, y, z)
}
pub fn ecef_to_wgs84(p: &GeoEcefPoint) -> (f64, f64, f64) {
let GeoEcefPoint { x, y, z } = *p;
if x == 0.0 && y == 0.0 && z == 0.0 {
return (0.0, 0.0, -WGS84_A);
}
let p_xy = (x * x + y * y).sqrt();
let lon = y.atan2(x);
let theta = (z * WGS84_A).atan2(p_xy * WGS84_B);
let sin_theta = theta.sin();
let cos_theta = theta.cos();
let lat_num = z + WGS84_E_PRIME_SQ * WGS84_B * sin_theta.powi(3);
let lat_den = p_xy - WGS84_E2 * WGS84_A * cos_theta.powi(3);
let mut lat = lat_num.atan2(lat_den);
for _ in 0..3 {
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let h = if p_xy > 1.0 {
p_xy / cos_lat - n
} else {
z / sin_lat - n * (1.0 - WGS84_E2)
};
let denom = 1.0 - WGS84_E2 * n / (n + h);
lat = z.atan2(p_xy * denom);
}
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let height = if p_xy > 1.0 {
p_xy / cos_lat - n
} else {
z / sin_lat - n * (1.0 - WGS84_E2)
};
(lat.to_degrees(), lon.to_degrees(), height)
}
#[cfg(test)]
mod tests {
use super::*;
const ROUNDTRIP_TOL_M: f64 = 1.0e-8;
const ANGLE_TOL_DEG: f64 = 1.0e-12;
fn approx_eq(a: f64, b: f64, tol: f64, label: &str) {
let diff = (a - b).abs();
assert!(diff <= tol, "{label}: {a} vs {b} (diff {diff} > tol {tol})");
}
#[test]
fn wgs84_to_ecef_at_equator_prime_meridian() {
let p = wgs84_to_ecef(0.0, 0.0, 0.0);
approx_eq(p.x, WGS84_A, 1e-9, "x");
approx_eq(p.y, 0.0, 1e-9, "y");
approx_eq(p.z, 0.0, 1e-9, "z");
}
#[test]
fn wgs84_to_ecef_at_north_pole() {
let p = wgs84_to_ecef(90.0, 42.0, 0.0);
approx_eq(p.x, 0.0, 1e-6, "x");
approx_eq(p.y, 0.0, 1e-6, "y");
approx_eq(p.z, WGS84_B, 1e-6, "z");
}
#[test]
fn wgs84_to_ecef_at_south_pole() {
let p = wgs84_to_ecef(-90.0, -123.0, 0.0);
approx_eq(p.x, 0.0, 1e-6, "x");
approx_eq(p.y, 0.0, 1e-6, "y");
approx_eq(p.z, -WGS84_B, 1e-6, "z");
}
#[test]
fn wgs84_to_ecef_height_on_equator() {
let p = wgs84_to_ecef(0.0, 0.0, 10_000.0);
approx_eq(p.x, WGS84_A + 10_000.0, 1e-9, "x");
approx_eq(p.y, 0.0, 1e-9, "y");
approx_eq(p.z, 0.0, 1e-9, "z");
}
#[test]
fn round_trip_equator_prime_meridian() {
let (lat, lon, h) = ecef_to_wgs84(&wgs84_to_ecef(0.0, 0.0, 0.0));
approx_eq(lat, 0.0, ANGLE_TOL_DEG, "lat");
approx_eq(lon, 0.0, ANGLE_TOL_DEG, "lon");
approx_eq(h, 0.0, ROUNDTRIP_TOL_M, "h");
}
#[test]
fn round_trip_tokyo() {
let lat0 = 35.6586;
let lon0 = 139.7454;
let h0 = 250.0;
let (lat, lon, h) = ecef_to_wgs84(&wgs84_to_ecef(lat0, lon0, h0));
approx_eq(lat, lat0, ANGLE_TOL_DEG, "lat");
approx_eq(lon, lon0, ANGLE_TOL_DEG, "lon");
approx_eq(h, h0, ROUNDTRIP_TOL_M, "h");
}
#[test]
fn round_trip_high_altitude_satellite_orbit() {
let lat0 = 51.6;
let lon0 = -0.1; let h0 = 400_000.0;
let (lat, lon, h) = ecef_to_wgs84(&wgs84_to_ecef(lat0, lon0, h0));
approx_eq(lat, lat0, ANGLE_TOL_DEG, "lat");
approx_eq(lon, lon0, ANGLE_TOL_DEG, "lon");
approx_eq(h, h0, ROUNDTRIP_TOL_M, "h");
}
#[test]
fn round_trip_below_ellipsoid() {
let lat0 = 11.35;
let lon0 = 142.20;
let h0 = -10_000.0;
let (lat, lon, h) = ecef_to_wgs84(&wgs84_to_ecef(lat0, lon0, h0));
approx_eq(lat, lat0, ANGLE_TOL_DEG, "lat");
approx_eq(lon, lon0, ANGLE_TOL_DEG, "lon");
approx_eq(h, h0, ROUNDTRIP_TOL_M, "h");
}
#[test]
fn round_trip_north_pole() {
let lat0 = 90.0;
let h0 = 100.0;
let p = wgs84_to_ecef(lat0, 0.0, h0);
let (lat, _lon, h) = ecef_to_wgs84(&p);
approx_eq(lat, lat0, ANGLE_TOL_DEG, "lat");
approx_eq(h, h0, 1e-3, "h"); }
#[test]
fn round_trip_south_pole() {
let lat0 = -90.0;
let h0 = 50.0;
let p = wgs84_to_ecef(lat0, 17.0, h0);
let (lat, _lon, h) = ecef_to_wgs84(&p);
approx_eq(lat, lat0, ANGLE_TOL_DEG, "lat");
approx_eq(h, h0, 1e-3, "h");
}
#[test]
fn ecef_to_wgs84_origin_returns_centre_point() {
let (lat, lon, h) = ecef_to_wgs84(&GeoEcefPoint::new(0.0, 0.0, 0.0));
approx_eq(lat, 0.0, ANGLE_TOL_DEG, "lat");
approx_eq(lon, 0.0, ANGLE_TOL_DEG, "lon");
approx_eq(h, -WGS84_A, 1e-9, "h");
}
#[test]
fn round_trip_random_grid() {
for &lat0 in &[-89.0, -45.0, -10.0, 0.0, 10.0, 45.0, 89.0] {
for &lon0 in &[-179.0, -120.0, -45.0, 0.0, 45.0, 120.0, 179.0] {
for &h0 in &[-5_000.0, 0.0, 100.0, 5_000.0, 100_000.0] {
let p = wgs84_to_ecef(lat0, lon0, h0);
let (lat, lon, h) = ecef_to_wgs84(&p);
approx_eq(lat, lat0, ANGLE_TOL_DEG, "lat");
approx_eq(lon, lon0, ANGLE_TOL_DEG, "lon");
approx_eq(h, h0, ROUNDTRIP_TOL_M, "h");
}
}
}
}
}