use core::fmt;
use crate::astro::math::vec3::{cross3, unit3};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum FrameValueError {
#[error("invalid frame value {field}: {reason}")]
InvalidInput {
field: &'static str,
reason: &'static str,
},
}
const fn invalid_input(field: &'static str, reason: &'static str) -> FrameValueError {
FrameValueError::InvalidInput { field, reason }
}
pub(crate) fn geocentric_up(position_ecef_m: [f64; 3]) -> [f64; 3] {
unit3(position_ecef_m).unwrap_or([0.0, 0.0, 1.0])
}
pub(crate) fn geocentric_east(up: [f64; 3]) -> [f64; 3] {
unit3(cross3([0.0, 0.0, 1.0], up)).unwrap_or([1.0, 0.0, 0.0])
}
pub(crate) fn geocentric_neu_basis(position_ecef_m: [f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]) {
let up = geocentric_up(position_ecef_m);
let east = geocentric_east(up);
let north = unit3(cross3(up, east)).unwrap_or([0.0, 0.0, 1.0]);
(north, east, up)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ItrfPositionM {
pub x_m: f64,
pub y_m: f64,
pub z_m: f64,
}
impl ItrfPositionM {
pub const fn new(x_m: f64, y_m: f64, z_m: f64) -> Result<Self, FrameValueError> {
if !x_m.is_finite() {
return Err(invalid_input("x_m", "must be finite"));
}
if !y_m.is_finite() {
return Err(invalid_input("y_m", "must be finite"));
}
if !z_m.is_finite() {
return Err(invalid_input("z_m", "must be finite"));
}
Ok(Self { x_m, y_m, z_m })
}
pub const fn as_array(self) -> [f64; 3] {
[self.x_m, self.y_m, self.z_m]
}
}
impl fmt::Display for ItrfPositionM {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ITRF[{:.3}, {:.3}, {:.3}] m",
self.x_m, self.y_m, self.z_m
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Wgs84Geodetic {
pub lat_rad: f64,
pub lon_rad: f64,
pub height_m: f64,
}
impl Wgs84Geodetic {
pub const fn new(lat_rad: f64, lon_rad: f64, height_m: f64) -> Result<Self, FrameValueError> {
if !lat_rad.is_finite() {
return Err(invalid_input("lat_rad", "must be finite"));
}
if !lon_rad.is_finite() {
return Err(invalid_input("lon_rad", "must be finite"));
}
if !height_m.is_finite() {
return Err(invalid_input("height_m", "must be finite"));
}
if lat_rad < -core::f64::consts::FRAC_PI_2 || lat_rad > core::f64::consts::FRAC_PI_2 {
return Err(invalid_input("lat_rad", "must be in [-pi/2, pi/2]"));
}
if lon_rad < -core::f64::consts::PI || lon_rad > core::f64::consts::PI {
return Err(invalid_input("lon_rad", "must be in [-pi, pi]"));
}
Ok(Self {
lat_rad,
lon_rad,
height_m,
})
}
}
impl fmt::Display for Wgs84Geodetic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"WGS84[lat {:.6} rad, lon {:.6} rad, h {:.3} m]",
self.lat_rad, self.lon_rad, self.height_m
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ItrfVelocityMS {
pub vx_m_s: f64,
pub vy_m_s: f64,
pub vz_m_s: f64,
}
impl ItrfVelocityMS {
pub const fn new(vx_m_s: f64, vy_m_s: f64, vz_m_s: f64) -> Result<Self, FrameValueError> {
if !vx_m_s.is_finite() {
return Err(invalid_input("vx_m_s", "must be finite"));
}
if !vy_m_s.is_finite() {
return Err(invalid_input("vy_m_s", "must be finite"));
}
if !vz_m_s.is_finite() {
return Err(invalid_input("vz_m_s", "must be finite"));
}
Ok(Self {
vx_m_s,
vy_m_s,
vz_m_s,
})
}
pub const fn as_array(self) -> [f64; 3] {
[self.vx_m_s, self.vy_m_s, self.vz_m_s]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::math::vec3::{norm3, scale3};
fn bits(v: [f64; 3]) -> [u64; 3] {
[v[0].to_bits(), v[1].to_bits(), v[2].to_bits()]
}
fn reference_neu(position_ecef_m: [f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]) {
let up = match norm3(position_ecef_m) {
n if n > 0.0 => scale3(position_ecef_m, 1.0 / n),
_ => [0.0, 0.0, 1.0],
};
let cross = cross3([0.0, 0.0, 1.0], up);
let east = if cross == [0.0, 0.0, 0.0] {
[1.0, 0.0, 0.0]
} else {
match norm3(cross) {
n if n > 0.0 => scale3(cross, 1.0 / n),
_ => [1.0, 0.0, 0.0],
}
};
let north = match norm3(cross3(up, east)) {
n if n > 0.0 => scale3(cross3(up, east), 1.0 / n),
_ => [0.0, 0.0, 1.0],
};
(north, east, up)
}
fn assert_close3(actual: [f64; 3], expected: [f64; 3], tol: f64) {
for (a, e) in actual.into_iter().zip(expected) {
assert!(
(a - e).abs() <= tol,
"component {a:?} differs from {e:?} by more than {tol}"
);
}
}
#[test]
fn constructors_reject_invalid_frame_values() {
assert_eq!(
ItrfPositionM::new(f64::NAN, 0.0, 0.0),
Err(FrameValueError::InvalidInput {
field: "x_m",
reason: "must be finite"
})
);
assert_eq!(
ItrfVelocityMS::new(0.0, f64::INFINITY, 0.0),
Err(FrameValueError::InvalidInput {
field: "vy_m_s",
reason: "must be finite"
})
);
assert_eq!(
Wgs84Geodetic::new(core::f64::consts::FRAC_PI_2 + 1.0e-12, 0.0, 0.0),
Err(FrameValueError::InvalidInput {
field: "lat_rad",
reason: "must be in [-pi/2, pi/2]"
})
);
assert_eq!(
Wgs84Geodetic::new(0.0, -core::f64::consts::PI - 1.0e-12, 0.0),
Err(FrameValueError::InvalidInput {
field: "lon_rad",
reason: "must be in [-pi, pi]"
})
);
assert_eq!(
Wgs84Geodetic::new(0.0, 0.0, f64::NAN),
Err(FrameValueError::InvalidInput {
field: "height_m",
reason: "must be finite"
})
);
}
#[test]
fn constructors_preserve_valid_frame_values() {
let position = ItrfPositionM::new(1.0, -2.0, 3.5).expect("valid ITRF position");
assert_eq!(position.as_array(), [1.0, -2.0, 3.5]);
let velocity = ItrfVelocityMS::new(0.1, -0.2, 0.3).expect("valid ITRF velocity");
assert_eq!(velocity.as_array(), [0.1, -0.2, 0.3]);
let geodetic =
Wgs84Geodetic::new(-0.5, core::f64::consts::PI, -12.0).expect("valid geodetic");
assert_eq!(geodetic.lat_rad.to_bits(), (-0.5_f64).to_bits());
assert_eq!(geodetic.lon_rad.to_bits(), core::f64::consts::PI.to_bits());
assert_eq!(geodetic.height_m.to_bits(), (-12.0_f64).to_bits());
let west_antimeridian =
Wgs84Geodetic::new(0.25, -core::f64::consts::PI, 42.0).expect("valid geodetic");
assert_eq!(
west_antimeridian.lon_rad.to_bits(),
(-core::f64::consts::PI).to_bits()
);
}
#[test]
fn geocentric_basis_matches_reference_recipe_to_the_bit() {
let position = [4_027_894.0, 307_045.0, 4_919_474.0];
let (north, east, up) = geocentric_neu_basis(position);
let (rn, re, ru) = reference_neu(position);
assert_eq!(bits(north), bits(rn));
assert_eq!(bits(east), bits(re));
assert_eq!(bits(up), bits(ru));
assert_eq!(bits(up), bits(geocentric_up(position)));
assert_eq!(bits(east), bits(geocentric_east(up)));
}
#[test]
fn geocentric_basis_is_right_handed_enu_and_points_north() {
let (north, east, up) = geocentric_neu_basis([6_378_137.0, 0.0, 0.0]);
assert_close3(up, [1.0, 0.0, 0.0], 1.0e-15);
assert_close3(east, [0.0, 1.0, 0.0], 1.0e-15);
assert_close3(north, [0.0, 0.0, 1.0], 1.0e-15);
let (north, east, up) = geocentric_neu_basis([4_027_894.0, 307_045.0, 4_919_474.0]);
assert_close3(cross3(up, east), north, 1.0e-15);
assert_close3(cross3(east, north), up, 1.0e-15);
}
#[test]
fn geocentric_basis_handles_degenerate_positions() {
let (north, east, up) = geocentric_neu_basis([0.0, 0.0, 0.0]);
assert_eq!(up, [0.0, 0.0, 1.0]);
assert_eq!(east, [1.0, 0.0, 0.0]);
assert_eq!(north, [0.0, 1.0, 0.0]);
assert_eq!(geocentric_east([0.0, 0.0, 1.0]), [1.0, 0.0, 0.0]);
}
}