use crate::angle::atan2d;
#[cfg(all(feature = "libm", not(feature = "std"), not(test)))]
use crate::math::Float as _;
use crate::position::{Ecef, Geodetic};
use crate::vector::{fma, Vec3};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ellipsoid {
a: f64,
f: f64,
}
pub const WGS84: Ellipsoid = Ellipsoid::new(6_378_137.0, 1.0 / 298.257_223_563);
pub const GRS80: Ellipsoid = Ellipsoid::new(6_378_137.0, 1.0 / 298.257_222_101);
pub const CGCS2000: Ellipsoid = Ellipsoid::new(6_378_137.0, 1.0 / 298.257_222_101);
pub const PZ90: Ellipsoid = Ellipsoid::new(6_378_136.0, 1.0 / 298.257_839_303);
pub const GSK_2011: Ellipsoid = Ellipsoid::new(6_378_136.5, 1.0 / 298.256_415_1);
#[doc(alias = "krassowsky")]
pub const KRASSOVSKY_1940: Ellipsoid = Ellipsoid::new(6_378_245.0, 1.0 / 298.3);
pub const IAG_1975: Ellipsoid = Ellipsoid::new(6_378_140.0, 1.0 / 298.257);
pub const WGS72: Ellipsoid = Ellipsoid::new(6_378_135.0, 1.0 / 298.26);
pub const WGS66: Ellipsoid = Ellipsoid::new(6_378_145.0, 1.0 / 298.25);
pub const WGS60: Ellipsoid = Ellipsoid::new(6_378_165.0, 1.0 / 298.3);
pub const GRS67: Ellipsoid = Ellipsoid::new(6_378_160.0, 1.0 / 298.247_167_427);
pub const AUSTRALIAN_NATIONAL: Ellipsoid = Ellipsoid::new(6_378_160.0, 1.0 / 298.25);
pub const INTERNATIONAL_1924: Ellipsoid = Ellipsoid::new(6_378_388.0, 1.0 / 297.0);
pub const AIRY_1830: Ellipsoid = Ellipsoid::new(6_377_563.396, 1.0 / 299.324_964_6);
pub const BESSEL_1841: Ellipsoid = Ellipsoid::new(6_377_397.155, 1.0 / 299.152_812_8);
pub const CLARKE_1866: Ellipsoid = Ellipsoid::from_axes(6_378_206.4, 6_356_583.8);
pub const EVEREST_1830: Ellipsoid = Ellipsoid::new(6_377_276.345, 1.0 / 300.801_7);
pub const EVEREST_1830_MODIFIED: Ellipsoid = Ellipsoid::new(6_377_304.063, 1.0 / 300.801_7);
impl Default for Ellipsoid {
fn default() -> Self {
WGS84
}
}
impl Ellipsoid {
#[inline]
pub const fn new(a: f64, f: f64) -> Self {
Ellipsoid { a, f }
}
#[inline]
pub const fn from_axes(a: f64, b: f64) -> Self {
Ellipsoid::new(a, (a - b) / a)
}
#[inline]
pub const fn a(self) -> f64 {
self.a
}
#[inline]
pub const fn f(self) -> f64 {
self.f
}
#[inline]
pub const fn b(self) -> f64 {
self.a * (1.0 - self.f)
}
#[inline]
pub const fn e2(self) -> f64 {
self.f * (2.0 - self.f)
}
#[inline]
pub const fn ep2(self) -> f64 {
let e2 = self.e2();
e2 / (1.0 - e2)
}
#[doc(alias = "geodetic2ecef")]
#[doc(alias = "lla2ecef")]
#[inline]
pub fn to_ecef(self, g: Geodetic) -> Ecef {
let (slat, clat) = g.lat_rad().sin_cos();
let (slon, clon) = g.lon_rad().sin_cos();
let e2 = self.e2();
let n = self.a / fma(e2, -(slat * slat), 1.0).sqrt();
let nh = n + g.height_m;
Ecef::new(nh * clat * clon, nh * clat * slon, fma(n, 1.0 - e2, g.height_m) * slat)
}
#[doc(alias = "ecef2geodetic")]
#[doc(alias = "ecef2lla")]
pub fn to_geodetic(self, e: Ecef) -> Geodetic {
let (a, f) = (self.a, self.f);
let e2 = self.e2();
let e2m = (1.0 - f) * (1.0 - f);
let (e2a, e4a) = (e2.abs(), e2 * e2);
let (x, y, z) = (e.x, e.y, e.z);
let r = fma(x, x, y * y).sqrt();
let (slam, clam) = if r != 0.0 { (y / r, x / r) } else { (0.0, 1.0) };
let mut h = fma(r, r, z * z).sqrt(); let (sphi, cphi);
if h.is_nan() || h > 2.0 * a / f64::EPSILON {
let rr = (x / 2.0).hypot(y / 2.0);
let hh = (z / 2.0).hypot(rr);
(sphi, cphi) = (z / 2.0 / hh, rr / hh);
} else if e4a == 0.0 {
let zz = if h == 0.0 { 1.0 } else { z };
let hh = zz.hypot(r);
(sphi, cphi) = (zz / hh, r / hh);
h -= a;
} else {
let (mut p, mut q) = ((r / a) * (r / a), e2m * (z / a) * (z / a));
let rr = (p + q - e4a) / 6.0;
if f < 0.0 {
core::mem::swap(&mut p, &mut q);
}
if !(e4a * q == 0.0 && rr <= 0.0) {
let s = e4a * p * q / 4.0;
let r2 = rr * rr;
let r3 = rr * r2;
let disc = s * (2.0 * r3 + s);
let mut u = rr;
if disc >= 0.0 {
let t3 = s + r3 + disc.sqrt().copysign(s + r3);
let t = t3.cbrt();
u += t + if t != 0.0 { r2 / t } else { 0.0 };
} else {
let ang = (-disc).sqrt().atan2(-(s + r3));
u += 2.0 * rr * (ang / 3.0).cos();
}
let v = (u * u + e4a * q).sqrt();
let uv = if u < 0.0 { e4a * q / (v - u) } else { u + v };
let w = (e2a * (uv - q) / (2.0 * v)).max(0.0);
let k = uv / ((uv + w * w).sqrt() + w);
let (k1, k2) = if f >= 0.0 { (k, k + e2) } else { (k - e2, k) };
let d = k1 * r / k2;
(sphi, cphi) = (z / k1, r / k2);
h = (1.0 - e2m / k1) * fma(d, d, z * z).sqrt();
} else {
let zz = ((if f >= 0.0 { e4a - p } else { p }) / e2m).sqrt();
let xx = (if f < 0.0 { e4a - p } else { p }).sqrt();
let hh = zz.hypot(xx);
(sphi, cphi) = ((zz / hh).copysign(if z < 0.0 { -1.0 } else { 1.0 }), xx / hh);
h = -a * (if f >= 0.0 { e2m } else { 1.0 }) * hh / e2a;
}
}
let lon = if r != 0.0 { atan2d(y, x) } else { atan2d(slam, clam) };
let mut lat = atan2d(sphi, cphi);
if h > -1.0e6 && h <= 2.0 * a / f64::EPSILON {
let (s, c) = lat.to_radians().sin_cos();
let w2 = 1.0 - e2 * s * s;
let n = a / w2.sqrt();
let (dp, dz) = (r - (n + h) * c, z - (n * (1.0 - e2) + h) * s);
let m = n * (1.0 - e2) / w2; lat += ((c * dz - s * dp) / (m + h)).to_degrees();
h += c * dp + s * dz;
}
Geodetic::new(lat, lon, h)
}
pub fn interpolate(self, a: Geodetic, b: Geodetic, t: f64) -> Geodetic {
let (pa, pb) = (self.to_ecef(a).vec(), self.to_ecef(b).vec());
self.to_geodetic(Ecef::from(pa + (pb - pa) * t))
}
#[inline]
pub fn surface_normal(self, g: Geodetic) -> Vec3 {
let (slat, clat) = g.lat_rad().sin_cos();
let (slon, clon) = g.lon_rad().sin_cos();
Vec3::new(clat * clon, clat * slon, slat)
}
#[inline]
pub const fn mean_radius(self) -> f64 {
(2.0 * self.a + self.b()) / 3.0
}
pub fn geocentric_radius(self, lat_rad: f64) -> f64 {
let (s, c) = lat_rad.sin_cos();
let (a, b) = (self.a, self.b());
let (ac, bs) = (a * c, b * s);
((a * ac * a * ac + b * bs * b * bs) / (ac * ac + bs * bs)).sqrt()
}
#[inline]
pub fn prime_vertical_radius(self, lat_rad: f64) -> f64 {
let s = lat_rad.sin();
self.a / (1.0 - self.e2() * s * s).sqrt()
}
#[inline]
pub fn meridional_radius(self, lat_rad: f64) -> f64 {
let s = lat_rad.sin();
let w = 1.0 - self.e2() * s * s;
self.a * (1.0 - self.e2()) / (w * w.sqrt())
}
#[inline]
pub fn geocentric_latitude(self, geodetic_lat_rad: f64) -> f64 {
((1.0 - self.e2()) * geodetic_lat_rad.tan()).atan()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{angle_close, close};
#[test]
fn derived_constants() {
assert!(close(WGS84.b(), 6_356_752.314_245, 1e-6));
assert!(close(WGS84.e2(), 0.006_694_379_990_14, 1e-12));
assert_eq!(Ellipsoid::default(), WGS84);
}
#[test]
fn ecef_hits_the_known_axis_points() {
let e = WGS84.to_ecef(Geodetic::new(0.0, 0.0, 0.0));
assert!(close(e.x, WGS84.a(), 1e-3) && close(e.y, 0.0, 1e-6) && close(e.z, 0.0, 1e-6));
let n = WGS84.to_ecef(Geodetic::new(90.0, 0.0, 0.0));
assert!(close(n.x, 0.0, 1e-3) && close(n.y, 0.0, 1e-3) && close(n.z, WGS84.b(), 1e-6));
let up = WGS84.to_ecef(Geodetic::new(0.0, 0.0, 1000.0));
assert!(close(up.x, WGS84.a() + 1000.0, 1e-3));
}
#[test]
fn geodetic_round_trips_on_every_ellipsoid() {
let pts = [
Geodetic::new(0.0, 0.0, 0.0),
Geodetic::new(34.05, -118.24, 100.0),
Geodetic::new(-33.87, 151.21, 2000.0),
Geodetic::new(89.9, 10.0, 500.0),
Geodetic::new(-89.99, -179.99, 0.0),
Geodetic::new(45.0, 90.0, 20_200_000.0), Geodetic::new(10.0, 20.0, -50.0), ];
for ell in [
WGS84,
GRS80,
WGS72,
INTERNATIONAL_1924,
AIRY_1830,
BESSEL_1841,
PZ90,
CLARKE_1866,
] {
for g in pts {
let r = ell.to_geodetic(ell.to_ecef(g));
assert!(
close(r.lat_deg, g.lat_deg, 1e-11)
&& close(r.lon_deg, g.lon_deg, 1e-11)
&& close(r.height_m, g.height_m, 1e-6),
"{ell:?} {g:?} -> {r:?}"
);
}
}
}
#[test]
fn dense_global_grid_round_trips_to_machine_precision() {
let mut lat = -89.5;
while lat <= 89.5 {
let mut lon = -180.0;
while lon < 180.0 {
for h in [-11_000.0, 0.0, 8_848.0, 12_000.0, 400_000.0, 20_200_000.0, 35_786_000.0] {
let g = Geodetic::new(lat, lon, h);
let r = WGS84.to_geodetic(WGS84.to_ecef(g));
assert!(
close(r.lat_deg, lat, 1e-11)
&& angle_close(r.lon_deg, lon, 1e-11)
&& close(r.height_m, h, 1e-5),
"{g:?} -> {r:?}"
);
}
lon += 15.0;
}
lat += 7.0;
}
}
#[test]
fn poles_are_handled_exactly() {
let n = WGS84.to_geodetic(Ecef::new(0.0, 0.0, WGS84.b() + 10.0));
assert!(close(n.lat_deg, 90.0, 1e-12) && close(n.height_m, 10.0, 1e-6));
let s = WGS84.to_geodetic(Ecef::new(0.0, 0.0, -WGS84.b()));
assert!(close(s.lat_deg, -90.0, 1e-12) && close(s.height_m, 0.0, 1e-6));
}
#[test]
fn deep_inside_and_far_out_round_trip() {
for lat in [-89.0, -60.0, -30.0, -1.0, 0.0, 1.0, 30.0, 60.0, 89.0] {
for h in [-6.0e6, -3.0e6, -1.0e6, 1.0e8, 1.0e9] {
let g = Geodetic::new(lat, 40.0, h);
let r = WGS84.to_geodetic(WGS84.to_ecef(g));
let tol_m = 4.0 * f64::EPSILON * (WGS84.a() + h.abs());
let tol_deg = (tol_m / (WGS84.a() + h).abs().max(1.0e5)).to_degrees();
assert!(
close(r.lat_deg, lat, tol_deg) && close(r.height_m, h, tol_m),
"{g:?} -> {r:?}"
);
}
}
}
#[test]
fn inside_the_evolute_matches_the_exact_answer() {
for lat in [10.0f64, 30.0, 60.0, 85.0] {
let (s, c) = lat.to_radians().sin_cos();
let w = (1.0 - WGS84.e2() * s * s).sqrt();
let r = WGS84.a() * WGS84.e2() * c / w;
let g = WGS84.to_geodetic(Ecef::new(r, 0.0, 0.0));
assert!(close(g.lat_deg, lat, 1e-10), "{lat}: {g:?}");
assert!(
close(g.height_m, -WGS84.a() * (1.0 - WGS84.e2()) / w, 1e-6),
"{lat}: {g:?}"
);
}
let c = WGS84.to_geodetic(Ecef::new(0.0, 0.0, 0.0));
assert_eq!((c.lat_deg, c.lon_deg, c.height_m), (90.0, 0.0, -WGS84.b()));
}
#[test]
fn spheres_prolate_ellipsoids_and_extremes() {
let sphere = Ellipsoid::new(6_371_000.0, 0.0);
let prolate = Ellipsoid::new(6_378_137.0, -1.0 / 150.0);
for ell in [sphere, prolate] {
for (lat, h) in [(0.0, 0.0), (45.0, 1_000.0), (-89.5, 20_000.0), (60.0, -500_000.0)] {
let g = Geodetic::new(lat, -120.0, h);
let r = ell.to_geodetic(ell.to_ecef(g));
assert!(
close(r.lat_deg, lat, 1e-11) && close(r.height_m, h, 1e-6),
"{ell:?} {g:?} -> {r:?}"
);
}
}
let far = WGS84.to_geodetic(Ecef::new(3.0e30, 0.0, 4.0e30));
assert!(close(far.lat_deg, 53.130_102_354_155_98, 1e-12) && close(far.height_m, 5.0e30, 1e15));
assert!(WGS84.to_geodetic(Ecef::new(f64::NAN, 0.0, 0.0)).lat_deg.is_nan());
}
#[test]
fn radii_and_latitudes() {
assert!(close(WGS84.mean_radius(), 6_371_008.771, 1e-3));
assert!(close(
WGS84.meridional_radius(0.0),
WGS84.a() * (1.0 - WGS84.e2()),
1e-6
));
assert!(close(
WGS84.meridional_radius(core::f64::consts::FRAC_PI_2),
WGS84.a() / (1.0 - WGS84.e2()).sqrt(),
1e-6
));
assert!(close(
WGS84.prime_vertical_radius(core::f64::consts::FRAC_PI_2),
WGS84.meridional_radius(core::f64::consts::FRAC_PI_2),
1e-6
));
assert!(close(WGS84.geocentric_radius(0.0), WGS84.a(), 1e-9));
assert!(close(
WGS84.geocentric_radius(core::f64::consts::FRAC_PI_2),
WGS84.b(),
1e-6
));
let g45 = WGS84.geocentric_latitude(45f64.to_radians());
assert!(g45 < 45f64.to_radians() && g45 > 44.8f64.to_radians());
assert_eq!(WGS84.geocentric_latitude(0.0), 0.0);
let n = WGS84.surface_normal(Geodetic::new(90.0, 0.0, 0.0));
assert!(close(n.z, 1.0, 1e-15) && close(n.norm(), 1.0, 1e-15));
}
#[test]
fn interpolation_is_linear_in_ecef() {
let a = Geodetic::new(0.0, 0.0, 1000.0);
let b = Geodetic::new(0.0, 0.01, 3000.0);
let m = WGS84.interpolate(a, b, 0.5);
assert!(close(m.lon_deg, 0.005, 1e-6));
let level = WGS84.interpolate(a, b.with_height(1000.0), 0.5);
assert!(close(level.lon_deg, 0.005, 1e-12), "{}", level.lon_deg);
assert!(m.height_m < 2000.0 && close(m.height_m, 2000.0, 0.1), "{}", m.height_m);
assert!(close(WGS84.interpolate(a, b, 0.0).height_m, 1000.0, 1e-6));
assert!(close(WGS84.interpolate(a, b, 1.0).lon_deg, 0.01, 1e-9));
}
}