use nalgebra::{ComplexField, Matrix3, RealField, Vector3};
use crate::frames::Ned;
use crate::units::{Position, RadiansPerSecond};
const WGS84_A: f64 = 6_378_137.0;
const WGS84_E2: f64 = 6.694_379_990_141e-3;
const WGS84_F: f64 = 1.0 / 298.257_223_563;
const WGS84_GAMMA_E: f64 = 9.780_325_335_9;
const WGS84_K: f64 = 1.931_852_652_458e-3;
const WGS84_M: f64 = 3.449_786_506_841e-3;
pub(crate) const EARTH_RATE: RadiansPerSecond = RadiansPerSecond::from_rad_per_s(7.292_115e-5);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Geodetic {
latitude: f64,
longitude: f64,
height: f64,
}
impl Geodetic {
pub fn from_degrees(latitude: f64, longitude: f64, height: f64) -> Self {
Self::from_radians(latitude.to_radians(), longitude.to_radians(), height)
}
pub const fn from_radians(latitude: f64, longitude: f64, height: f64) -> Self {
Self {
latitude,
longitude,
height,
}
}
pub fn from_degrees_e7(latitude: i32, longitude: i32, height_mm: i32) -> Self {
Self::from_degrees(
f64::from(latitude) * 1e-7,
f64::from(longitude) * 1e-7,
f64::from(height_mm) * 1e-3,
)
}
pub fn latitude_deg(self) -> f64 {
self.latitude.to_degrees()
}
pub fn longitude_deg(self) -> f64 {
self.longitude.to_degrees()
}
pub const fn latitude_rad(self) -> f64 {
self.latitude
}
pub const fn longitude_rad(self) -> f64 {
self.longitude
}
pub const fn height(self) -> f64 {
self.height
}
#[cfg(feature = "magnetic-model")]
pub fn magnetic_declination(self) -> Option<crate::units::Radians> {
crate::magnetic::declination_at(self)
}
pub fn normal_gravity(self) -> f32 {
let sin_phi = ComplexField::sin(self.latitude);
let sin2 = sin_phi * sin_phi;
let surface =
WGS84_GAMMA_E * (1.0 + WGS84_K * sin2) / ComplexField::sqrt(1.0 - WGS84_E2 * sin2);
let h = self.height;
let first = 2.0 / WGS84_A * (1.0 + WGS84_F + WGS84_M - 2.0 * WGS84_F * sin2) * h;
let second = 3.0 / (WGS84_A * WGS84_A) * h * h;
(surface * (1.0 - first + second)) as f32
}
pub(crate) fn is_finite(self) -> bool {
self.latitude.is_finite() && self.longitude.is_finite() && self.height.is_finite()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LocalOrigin {
origin: Geodetic,
ecef: Vector3<f64>,
ecef_to_ned: Matrix3<f64>,
}
impl LocalOrigin {
pub fn new(origin: Geodetic) -> Option<Self> {
Self::is_usable(origin).then(|| Self::about(origin))
}
fn about(origin: Geodetic) -> Self {
let (sin_phi, cos_phi) = ComplexField::sin_cos(origin.latitude);
let (sin_lambda, cos_lambda) = ComplexField::sin_cos(origin.longitude);
#[rustfmt::skip]
let ecef_to_ned = Matrix3::new(
-sin_phi * cos_lambda, -sin_phi * sin_lambda, cos_phi,
-sin_lambda, cos_lambda, 0.0,
-cos_phi * cos_lambda, -cos_phi * sin_lambda, -sin_phi,
);
Self {
origin,
ecef: ecef(origin),
ecef_to_ned,
}
}
pub const fn geodetic(&self) -> Geodetic {
self.origin
}
pub fn to_ned(&self, point: Geodetic) -> Position<Ned> {
let ned = self.ecef_to_ned * (ecef(point) - self.ecef);
Position::from_vector(ned.map(|meters| meters as f32))
}
pub fn to_geodetic(&self, position: Position<Ned>) -> Geodetic {
let ned = position.vector().map(f64::from);
geodetic(self.ecef + self.ecef_to_ned.transpose() * ned)
}
pub fn placing(fix: Geodetic, estimate: Position<Ned>) -> Option<Self> {
const PASSES: usize = 5;
if !Self::is_usable(fix) {
return None;
}
let offset = estimate.vector().map(f64::from);
let fix = ecef(fix);
let mut origin = Self::about(geodetic(fix));
for _ in 0..PASSES {
origin = Self::about(geodetic(fix - origin.ecef_to_ned.transpose() * offset));
}
let landed = origin.ecef_to_ned * (fix - origin.ecef);
let tolerance = f64::max(1e-3, f64::from(f32::EPSILON) * offset.norm());
((landed - offset).norm() <= tolerance).then_some(origin)
}
fn is_usable(origin: Geodetic) -> bool {
origin.is_finite() && origin.latitude.abs() <= core::f64::consts::FRAC_PI_2
}
}
fn ecef(point: Geodetic) -> Vector3<f64> {
let (sin_phi, cos_phi) = ComplexField::sin_cos(point.latitude);
let (sin_lambda, cos_lambda) = ComplexField::sin_cos(point.longitude);
let n = prime_vertical_radius(sin_phi);
let h = point.height;
Vector3::new(
(n + h) * cos_phi * cos_lambda,
(n + h) * cos_phi * sin_lambda,
(n * (1.0 - WGS84_E2) + h) * sin_phi,
)
}
fn geodetic(ecef: Vector3<f64>) -> Geodetic {
const PASSES: usize = 5;
let p = ComplexField::hypot(ecef.x, ecef.y);
let longitude = RealField::atan2(ecef.y, ecef.x);
let mut latitude = RealField::atan2(ecef.z, p);
for _ in 0..PASSES {
let sin_phi = ComplexField::sin(latitude);
let n = prime_vertical_radius(sin_phi);
latitude = RealField::atan2(ecef.z + WGS84_E2 * n * sin_phi, p);
}
let (sin_phi, cos_phi) = ComplexField::sin_cos(latitude);
let height = p * cos_phi + ecef.z * sin_phi
- WGS84_A * ComplexField::sqrt(1.0 - WGS84_E2 * sin_phi * sin_phi);
Geodetic {
latitude,
longitude,
height,
}
}
fn prime_vertical_radius(sin_phi: f64) -> f64 {
WGS84_A / ComplexField::sqrt(1.0 - WGS84_E2 * sin_phi * sin_phi)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_gravity_meets_the_standards_own_figures() {
let at = |latitude, height| Geodetic::from_degrees(latitude, 0.0, height).normal_gravity();
assert_eq!(at(0.0, 0.0), 9.780_325_f32);
assert_eq!(at(90.0, 0.0), 9.832_185_f32);
assert_eq!(at(-90.0, 0.0), at(90.0, 0.0));
assert!((at(45.0, 0.0) - 9.806_2).abs() < 1e-4, "{}", at(45.0, 0.0));
let lapse = at(45.0, 0.0) - at(45.0, 1000.0);
assert!((lapse - 3.086e-3).abs() < 1e-5, "{lapse}");
let bend = 20.0 * lapse - (at(45.0, 0.0) - at(45.0, 20_000.0));
assert!((bend - 2.75e-4).abs() < 0.3e-4, "{bend}");
assert!(at(f64::NAN, 0.0).is_nan());
}
fn zurich() -> Geodetic {
Geodetic::from_degrees(47.3977, 8.5456, 488.0)
}
#[test]
fn the_origin_is_zero() {
let origin = LocalOrigin::new(zurich()).expect("usable");
assert!(origin.to_ned(zurich()).vector().norm() < 1e-6);
}
#[test]
fn ecef_matches_the_ellipsoid_where_it_is_known_by_heart() {
let equator = ecef(Geodetic::from_degrees(0.0, 0.0, 0.0));
assert!((equator - Vector3::new(WGS84_A, 0.0, 0.0)).norm() < 1e-6);
let b = 6_356_752.314_245;
let pole = ecef(Geodetic::from_degrees(90.0, 0.0, 0.0));
assert!((pole - Vector3::new(0.0, 0.0, b)).norm() < 1e-3, "{pole:?}");
}
#[test]
fn ecef_and_back_is_lossless_everywhere() {
for (lat, lon, h) in [
(47.3977, 8.5456, 488.0),
(-33.9, 151.2, -30.0),
(89.9999, 45.0, 1000.0),
(90.0, 0.0, 0.0),
(-90.0, 0.0, 0.0),
(0.0, -179.9999, 12_000.0),
(64.1, -21.9, 400_000.0),
] {
let point = Geodetic::from_degrees(lat, lon, h);
let back = geodetic(ecef(point));
let error = (ecef(back) - ecef(point)).norm();
assert!(error < 1e-6, "{lat}, {lon}, {h}: {error} m");
}
}
#[test]
fn a_round_trip_returns_what_went_in_at_any_range() {
let origin = LocalOrigin::new(zurich()).expect("usable");
for p in [
Position::ned(1234.5, -876.25, -120.0),
Position::ned(10_000.0, 10_000.0, 0.0),
Position::ned(-100_000.0, 60_000.0, -3000.0),
] {
let back = origin.to_ned(origin.to_geodetic(p));
let error = (back.vector() - p.vector()).norm();
assert!(error < 1e-2, "{p:?}: {error} m");
}
}
#[test]
fn range_is_the_straight_line_distance() {
let origin = LocalOrigin::new(zurich()).expect("usable");
let far = Geodetic::from_degrees(47.9, 9.3, 1200.0);
let chord = (ecef(far) - ecef(zurich())).norm();
let ned = f64::from(origin.to_ned(far).vector().norm());
assert!((ned - chord).abs() < 1e-2, "{ned} vs {chord}");
}
#[test]
fn it_matches_the_converter() {
let origin = LocalOrigin::new(zurich()).expect("usable");
for (point, expected) in [
(
Geodetic::from_degrees(47.4077, 8.5556, 498.0),
[1111.9223, 754.814, -9.8584],
),
(
Geodetic::from_degrees(47.4877, 8.6756, 488.0),
[10015.096, 9797.691, 15.3833],
),
] {
let p = origin.to_ned(point).to_array();
for (got, want) in p.into_iter().zip(expected) {
assert!((got - want).abs() < 2e-3, "{p:?} vs {expected:?}");
}
}
}
#[test]
fn at_range_the_plane_rises_off_the_surface() {
let origin = LocalOrigin::new(zurich()).expect("usable");
let down = origin
.to_ned(Geodetic::from_degrees(47.4877, 8.5456, 488.0))
.z();
assert!((7.0..9.0).contains(&down), "{down} m below the plane");
}
#[test]
fn up_is_negative_down() {
let origin = LocalOrigin::new(zurich()).expect("usable");
let above = Geodetic::from_degrees(47.3977, 8.5456, 500.0);
assert!((origin.to_ned(above).z() + 12.0).abs() < 1e-5);
}
#[test]
fn crossing_the_antimeridian_is_a_short_step() {
let origin =
LocalOrigin::new(Geodetic::from_degrees(-17.0, 179.9999, 0.0)).expect("usable");
let east = origin.to_ned(Geodetic::from_degrees(-17.0, -179.9999, 0.0));
let e = east.y();
assert!(e > 0.0 && e < 25.0, "0.0002° east, got {e} m");
}
#[test]
fn the_integer_encoding_is_the_same_position() {
let e7 = Geodetic::from_degrees_e7(473_977_000, 85_456_000, 488_000);
let p = LocalOrigin::new(zurich())
.expect("usable")
.to_ned(e7)
.vector();
assert!(p.norm() < 1e-3, "{p:?}");
}
#[test]
fn placing_puts_the_fix_at_the_estimate_even_far_out() {
for estimate in [
Position::ned(35.0, -12.0, -4.0),
Position::ned(8_000.0, -6_000.0, -300.0),
Position::ned(210_000.0, 210_000.0, -100.0),
] {
let origin = LocalOrigin::placing(zurich(), estimate).expect("usable");
let p = origin.to_ned(zurich());
assert!(
(p.vector() - estimate.vector()).norm() < 0.05,
"{p:?} != {estimate:?}"
);
}
}
#[test]
fn a_pole_is_an_origin_and_nonsense_is_not() {
let pole = Geodetic::from_degrees(90.0, 0.0, 0.0);
let origin = LocalOrigin::new(pole).expect("a pole is an origin");
let p = origin.to_ned(origin.to_geodetic(Position::ned(-1000.0, 0.0, 0.0)));
assert!((p.x() + 1000.0).abs() < 1e-3 && p.y().abs() < 1e-3, "{p:?}");
assert_eq!(
LocalOrigin::new(Geodetic::from_degrees(f64::NAN, 0.0, 0.0)),
None
);
assert_eq!(
LocalOrigin::new(Geodetic::from_degrees(91.0, 0.0, 0.0)),
None
);
}
#[test]
fn placing_refuses_an_origin_that_does_not_exist() {
let near_pole = Geodetic::from_degrees(89.9999, 90.0, 0.0);
assert_eq!(
LocalOrigin::placing(near_pole, Position::ned(0.0, 100.0, 0.0)),
None
);
let near_pole = Geodetic::from_degrees(89.999, 90.0, 0.0);
assert_eq!(
LocalOrigin::placing(near_pole, Position::ned(0.0, 300.0, 0.0)),
None
);
let estimate = Position::ned(-100.0, 0.0, 0.0);
let origin = LocalOrigin::placing(near_pole, estimate).expect("100 m south exists");
let p = origin.to_ned(near_pole);
assert!((p.vector() - estimate.vector()).norm() < 1e-3, "{p:?}");
}
}