mod datum;
use core::fmt;
use crate::error::{KernelError, Result};
use crate::math;
use crate::position::{Latitude, Longitude, Position};
use crate::units::Distance;
pub use datum::{Datum, Helmert};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "StoredEllipsoid", into = "StoredEllipsoid")
)]
pub struct Ellipsoid {
semi_major_metres: f64,
inverse_flattening: f64,
}
impl Ellipsoid {
pub const WGS84: Self = Self {
semi_major_metres: 6_378_137.0,
inverse_flattening: 298.257_223_563,
};
pub const GRS80: Self = Self {
semi_major_metres: 6_378_137.0,
inverse_flattening: 298.257_222_101,
};
pub const INTERNATIONAL_1924: Self = Self {
semi_major_metres: 6_378_388.0,
inverse_flattening: 297.0,
};
pub const CLARKE_1866: Self = Self {
semi_major_metres: 6_378_206.4,
inverse_flattening: 294.978_698_213_898,
};
pub const AIRY_1830: Self = Self {
semi_major_metres: 6_377_563.396,
inverse_flattening: 299.324_964_6,
};
pub const KRASSOWSKY_1940: Self = Self {
semi_major_metres: 6_378_245.0,
inverse_flattening: 298.3,
};
pub const BESSEL_1841: Self = Self {
semi_major_metres: 6_377_397.155,
inverse_flattening: 299.152_812_8,
};
pub const AUSTRALIAN_NATIONAL: Self = Self {
semi_major_metres: 6_378_160.0,
inverse_flattening: 298.25,
};
pub fn new(semi_major_axis: Distance, inverse_flattening: f64) -> Result<Self> {
Self::from_raw(semi_major_axis.metres(), inverse_flattening)
}
fn from_raw(semi_major_metres: f64, inverse_flattening: f64) -> Result<Self> {
if semi_major_metres.is_nan() || semi_major_metres <= 0.0 {
return Err(KernelError::OutOfRange {
parameter: "semi-major axis",
value: semi_major_metres,
min: f64::MIN_POSITIVE,
max: f64::MAX,
});
}
if inverse_flattening.is_nan() || inverse_flattening < 1.0 {
return Err(KernelError::OutOfRange {
parameter: "inverse flattening",
value: inverse_flattening,
min: 1.0,
max: f64::INFINITY,
});
}
Ok(Self {
semi_major_metres,
inverse_flattening,
})
}
#[must_use]
pub fn semi_major_axis(&self) -> Distance {
Distance::from_metres(self.semi_major_metres).unwrap_or(Distance::ZERO)
}
#[must_use]
pub fn semi_minor_axis(&self) -> Distance {
Distance::from_metres(self.semi_minor_metres()).unwrap_or(Distance::ZERO)
}
#[must_use]
pub fn flattening(&self) -> f64 {
1.0 / self.inverse_flattening
}
#[must_use]
pub const fn inverse_flattening(&self) -> f64 {
self.inverse_flattening
}
#[must_use]
pub fn first_eccentricity_squared(&self) -> f64 {
let f = self.flattening();
f * (2.0 - f)
}
fn semi_minor_metres(&self) -> f64 {
self.semi_major_metres * (1.0 - self.flattening())
}
fn second_eccentricity_squared(&self) -> f64 {
let e2 = self.first_eccentricity_squared();
e2 / (1.0 - e2)
}
fn prime_vertical_radius(&self, sin_latitude: f64) -> f64 {
self.semi_major_metres
/ math::sqrt(1.0 - self.first_eccentricity_squared() * sin_latitude * sin_latitude)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum VerticalDatum {
Ellipsoid,
MeanSeaLevel,
ChartDatum,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredEllipsoid {
semi_major_metres: f64,
inverse_flattening: f64,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredEllipsoid> for Ellipsoid {
type Error = KernelError;
fn try_from(stored: StoredEllipsoid) -> Result<Self> {
Self::from_raw(stored.semi_major_metres, stored.inverse_flattening)
}
}
#[cfg(feature = "serde")]
impl From<Ellipsoid> for StoredEllipsoid {
fn from(ellipsoid: Ellipsoid) -> Self {
Self {
semi_major_metres: ellipsoid.semi_major_metres,
inverse_flattening: ellipsoid.inverse_flattening,
}
}
}
impl VerticalDatum {
const fn name(self) -> &'static str {
match self {
Self::Ellipsoid => "the ellipsoid",
Self::MeanSeaLevel => "mean sea level",
Self::ChartDatum => "chart datum",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Height {
value: Distance,
datum: VerticalDatum,
}
impl Height {
#[must_use]
pub const fn above_ellipsoid(value: Distance) -> Self {
Self {
value,
datum: VerticalDatum::Ellipsoid,
}
}
#[must_use]
pub const fn above_mean_sea_level(value: Distance) -> Self {
Self {
value,
datum: VerticalDatum::MeanSeaLevel,
}
}
#[must_use]
pub const fn above_chart_datum(value: Distance) -> Self {
Self {
value,
datum: VerticalDatum::ChartDatum,
}
}
#[must_use]
pub const fn new(value: Distance, datum: VerticalDatum) -> Self {
Self { value, datum }
}
#[must_use]
pub const fn value(&self) -> Distance {
self.value
}
#[must_use]
pub const fn datum(&self) -> VerticalDatum {
self.datum
}
}
impl fmt::Display for Height {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let precision = f.precision().unwrap_or(1);
write!(
f,
"{:.*} m above {}",
precision,
self.value.metres(),
self.datum.name()
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GeodeticPoint {
position: Position,
height: Height,
}
impl GeodeticPoint {
#[must_use]
pub const fn new(position: Position, height: Height) -> Self {
Self { position, height }
}
#[must_use]
pub const fn position(&self) -> Position {
self.position
}
#[must_use]
pub const fn height(&self) -> Height {
self.height
}
}
impl fmt::Display for GeodeticPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}", self.position, self.height)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "StoredEcefPoint", into = "StoredEcefPoint")
)]
pub struct EcefPoint {
x: f64,
y: f64,
z: f64,
}
impl EcefPoint {
#[must_use]
pub fn new(x: Distance, y: Distance, z: Distance) -> Self {
Self {
x: x.metres(),
y: y.metres(),
z: z.metres(),
}
}
#[must_use]
pub fn x(&self) -> Distance {
Distance::from_metres(self.x).unwrap_or(Distance::ZERO)
}
#[must_use]
pub fn y(&self) -> Distance {
Distance::from_metres(self.y).unwrap_or(Distance::ZERO)
}
#[must_use]
pub fn z(&self) -> Distance {
Distance::from_metres(self.z).unwrap_or(Distance::ZERO)
}
pub fn from_geodetic(point: GeodeticPoint, ellipsoid: &Ellipsoid) -> Result<Self> {
if point.height.datum != VerticalDatum::Ellipsoid {
return Err(KernelError::VerticalDatumMismatch {
required: VerticalDatum::Ellipsoid,
found: point.height.datum,
});
}
let (sin_lat, cos_lat) = sin_cos(point.position.latitude().radians());
let (sin_lon, cos_lon) = sin_cos(point.position.longitude().radians());
let n = ellipsoid.prime_vertical_radius(sin_lat);
let h = point.height.value.metres();
let e2 = ellipsoid.first_eccentricity_squared();
Ok(Self {
x: (n + h) * cos_lat * cos_lon,
y: (n + h) * cos_lat * sin_lon,
z: (n * (1.0 - e2) + h) * sin_lat,
})
}
#[allow(clippy::many_single_char_names)]
pub fn to_geodetic(self, ellipsoid: &Ellipsoid) -> Result<GeodeticPoint> {
let a = ellipsoid.semi_major_metres;
let b = ellipsoid.semi_minor_metres();
let e2 = ellipsoid.first_eccentricity_squared();
let ep2 = ellipsoid.second_eccentricity_squared();
let p = math::hypot(self.x, self.y);
let r = math::hypot(p, self.z);
if r < f64::MIN_POSITIVE {
return Err(KernelError::Indeterminate {
quantity: "the geodetic position of the Earth's centre",
});
}
let longitude = Longitude::from_degrees(math::to_degrees(math::atan2(self.y, self.x)))?;
if p < f64::MIN_POSITIVE * a {
let latitude = if self.z < 0.0 {
Latitude::SOUTH_POLE
} else {
Latitude::NORTH_POLE
};
let height = Distance::from_metres(math::abs(self.z) - b)?;
return Ok(GeodeticPoint::new(
Position::new(latitude, longitude),
Height::above_ellipsoid(height),
));
}
let tan_u = (b * self.z / (a * p)) * (1.0 + ep2 * b / r);
let cos_u = 1.0 / math::sqrt(1.0 + tan_u * tan_u);
let sin_u = tan_u * cos_u;
let latitude_radians = math::atan2(
self.z + ep2 * b * sin_u * sin_u * sin_u,
p - e2 * a * cos_u * cos_u * cos_u,
);
let (sin_lat, cos_lat) = sin_cos(latitude_radians);
let n = ellipsoid.prime_vertical_radius(sin_lat);
let height_metres = p * cos_lat + self.z * sin_lat - a * a / n;
let latitude = Latitude::from_degrees(math::to_degrees(latitude_radians))?;
let height = Distance::from_metres(height_metres)?;
Ok(GeodeticPoint::new(
Position::new(latitude, longitude),
Height::above_ellipsoid(height),
))
}
#[must_use]
pub fn chord_to(&self, other: Self) -> Distance {
let chord = math::hypot(
math::hypot(other.x - self.x, other.y - self.y),
other.z - self.z,
);
Distance::from_metres(chord).unwrap_or(Distance::ZERO)
}
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredEcefPoint {
x: f64,
y: f64,
z: f64,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredEcefPoint> for EcefPoint {
type Error = KernelError;
fn try_from(stored: StoredEcefPoint) -> Result<Self> {
Ok(Self::new(
Distance::from_metres(stored.x)?,
Distance::from_metres(stored.y)?,
Distance::from_metres(stored.z)?,
))
}
}
#[cfg(feature = "serde")]
impl From<EcefPoint> for StoredEcefPoint {
fn from(point: EcefPoint) -> Self {
Self {
x: point.x,
y: point.y,
z: point.z,
}
}
}
fn sin_cos(radians: f64) -> (f64, f64) {
(math::sin(radians), math::cos(radians))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use alloc::format;
fn point(latitude: f64, longitude: f64, height: f64) -> GeodeticPoint {
GeodeticPoint::new(
Position::new(
Latitude::from_degrees(latitude).unwrap(),
Longitude::from_degrees(longitude).unwrap(),
),
Height::above_ellipsoid(Distance::from_metres(height).unwrap()),
)
}
#[test]
fn wgs84_derived_constants_match_the_published_ones() {
let e = Ellipsoid::WGS84;
assert!((e.semi_minor_axis().metres() - 6_356_752.314_245).abs() < 1e-6);
assert!((e.first_eccentricity_squared() - 6.694_379_990_14e-3).abs() < 1e-14);
assert!((e.second_eccentricity_squared() - 6.739_496_742_28e-3).abs() < 1e-14);
assert_eq!(e.inverse_flattening(), 298.257_223_563);
assert!((Ellipsoid::GRS80.semi_minor_axis().metres() - 6_356_752.314_140).abs() < 1e-6);
}
#[test]
fn an_ellipsoid_must_be_a_plausible_shape() {
assert!(Ellipsoid::new(Distance::from_metres(0.0).unwrap(), 300.0).is_err());
assert!(Ellipsoid::new(Distance::from_metres(6.4e6).unwrap(), 0.5).is_err());
assert!(Ellipsoid::new(Distance::from_metres(6.4e6).unwrap(), f64::NAN).is_err());
let sphere =
Ellipsoid::new(Distance::from_metres(6_371_000.0).unwrap(), f64::INFINITY).unwrap();
assert_eq!(sphere.flattening(), 0.0);
assert_eq!(sphere.semi_minor_axis().metres(), 6_371_000.0);
}
#[test]
fn ecef_of_reference_points_matches_the_textbook() {
let origin = EcefPoint::from_geodetic(point(0.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
assert!((origin.x().metres() - 6_378_137.0).abs() < 1e-6);
assert!(origin.y().metres().abs() < 1e-9);
assert!(origin.z().metres().abs() < 1e-9);
let pole = EcefPoint::from_geodetic(point(90.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
assert!(pole.x().metres().abs() < 1e-6);
assert!((pole.z().metres() - 6_356_752.314_245).abs() < 1e-6);
let inland =
EcefPoint::from_geodetic(point(34.0, -117.0, 251.0), &Ellipsoid::WGS84).unwrap();
assert!((inland.x().metres() - -2_403_183.467).abs() < 1e-3);
assert!((inland.y().metres() - -4_716_513.119).abs() < 1e-3);
assert!((inland.z().metres() - 3_546_586.921).abs() < 1e-3);
let diagonal =
EcefPoint::from_geodetic(point(45.0, 45.0, 1000.0), &Ellipsoid::WGS84).unwrap();
assert!((diagonal.x().metres() - 3_194_919.145).abs() < 1e-3);
assert!((diagonal.x().metres() - diagonal.y().metres()).abs() < 1e-6);
assert!((diagonal.z().metres() - 4_488_055.516).abs() < 1e-3);
}
#[test]
fn the_round_trip_holds_at_the_awkward_places() {
let places = [
(0.0, 0.0, 0.0),
(90.0, 0.0, 0.0),
(-90.0, 45.0, 1000.0),
(89.999_999, 179.999_999, -50.0),
(-45.0, -180.0, 20_200_000.0),
(50.755, -1.333, 48.0),
(1e-9, 1e-9, 0.0),
];
for (latitude, longitude, height) in places {
let there = point(latitude, longitude, height);
let back = EcefPoint::from_geodetic(there, &Ellipsoid::WGS84)
.unwrap()
.to_geodetic(&Ellipsoid::WGS84)
.unwrap();
assert!(
(back.position().latitude().degrees() - latitude).abs() < 1e-9,
"latitude at {latitude} {longitude} {height}: {}",
back.position().latitude().degrees()
);
assert!(
back.position()
.longitude_difference(there.position())
.degrees()
.abs()
< 1e-9
|| latitude.abs() == 90.0,
"longitude at {latitude} {longitude} {height}"
);
assert!(
(back.height().value().metres() - height).abs() < 1e-3,
"height at {latitude} {longitude} {height}: {}",
back.height().value().metres()
);
}
}
#[test]
fn a_sea_level_height_does_not_pretend_to_be_ellipsoidal() {
let msl = GeodeticPoint::new(
point(50.0, 0.0, 0.0).position(),
Height::above_mean_sea_level(Distance::from_metres(10.0).unwrap()),
);
assert_eq!(
EcefPoint::from_geodetic(msl, &Ellipsoid::WGS84),
Err(KernelError::VerticalDatumMismatch {
required: VerticalDatum::Ellipsoid,
found: VerticalDatum::MeanSeaLevel,
})
);
assert_eq!(format!("{}", msl.height()), "10.0 m above mean sea level");
}
#[test]
fn the_centre_of_the_earth_has_no_position() {
let centre = EcefPoint::new(Distance::ZERO, Distance::ZERO, Distance::ZERO);
assert!(matches!(
centre.to_geodetic(&Ellipsoid::WGS84),
Err(KernelError::Indeterminate { .. })
));
}
#[test]
fn the_chord_is_the_straight_line_through_the_earth() {
let north = EcefPoint::from_geodetic(point(90.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
let south = EcefPoint::from_geodetic(point(-90.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
assert!((north.chord_to(south).metres() - 2.0 * 6_356_752.314_245).abs() < 1e-6);
}
}