mod ecef;
mod ellipsoid;
mod hemisphere;
mod llh;
mod ned;
pub use ecef::*;
pub use ellipsoid::*;
pub use hemisphere::*;
pub use llh::*;
use nalgebra::Vector2;
pub use ned::*;
use crate::{reference_frame::ReferenceFrame, time::GpsTime};
use std::fmt;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)]
pub struct AzimuthElevation(Vector2<f64>);
impl AzimuthElevation {
#[must_use]
pub fn new(az: f64, el: f64) -> AzimuthElevation {
Self(Vector2::new(az, el))
}
#[must_use]
pub fn as_array(&self) -> &[f64; 2] {
&self.0.data.0[0]
}
#[must_use]
pub fn as_array_mut(&mut self) -> &mut [f64; 2] {
&mut self.0.data.0[0]
}
#[must_use]
pub fn as_vector(&self) -> &Vector2<f64> {
&self.0
}
#[must_use]
pub fn as_vector_mut(&mut self) -> &mut Vector2<f64> {
&mut self.0
}
#[must_use]
pub fn az(&self) -> f64 {
self.0.x
}
#[must_use]
pub fn el(&self) -> f64 {
self.0.y
}
}
impl From<[f64; 2]> for AzimuthElevation {
fn from(array: [f64; 2]) -> Self {
Self::new(array[0], array[1])
}
}
impl From<&[f64; 2]> for AzimuthElevation {
fn from(array: &[f64; 2]) -> Self {
Self::new(array[0], array[1])
}
}
impl From<Vector2<f64>> for AzimuthElevation {
fn from(vector: Vector2<f64>) -> Self {
Self(vector)
}
}
impl From<(f64, f64)> for AzimuthElevation {
fn from((x, y): (f64, f64)) -> Self {
Self::new(x, y)
}
}
impl AsRef<[f64; 2]> for AzimuthElevation {
fn as_ref(&self) -> &[f64; 2] {
self.as_array()
}
}
impl AsRef<Vector2<f64>> for AzimuthElevation {
fn as_ref(&self) -> &Vector2<f64> {
self.as_vector()
}
}
impl AsMut<[f64; 2]> for AzimuthElevation {
fn as_mut(&mut self) -> &mut [f64; 2] {
self.as_array_mut()
}
}
impl AsMut<Vector2<f64>> for AzimuthElevation {
fn as_mut(&mut self) -> &mut Vector2<f64> {
self.as_vector_mut()
}
}
impl fmt::Display for AzimuthElevation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AzimuthElevation {{ az: {}, el: {} }}",
self.az(),
self.el()
)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
pub struct Coordinate {
reference_frame: ReferenceFrame,
position: ECEF,
velocity: Option<ECEF>,
epoch: GpsTime,
}
impl Coordinate {
#[must_use]
pub fn new(
reference_frame: ReferenceFrame,
position: ECEF,
velocity: Option<ECEF>,
epoch: GpsTime,
) -> Self {
Coordinate {
reference_frame,
position,
velocity,
epoch,
}
}
#[must_use]
pub fn without_velocity(
reference_frame: ReferenceFrame,
position: ECEF,
epoch: GpsTime,
) -> Self {
Coordinate {
reference_frame,
position,
velocity: None,
epoch,
}
}
#[must_use]
pub fn with_velocity(
reference_frame: ReferenceFrame,
position: ECEF,
velocity: ECEF,
epoch: GpsTime,
) -> Self {
Coordinate {
reference_frame,
position,
velocity: Some(velocity),
epoch,
}
}
#[must_use]
pub fn reference_frame(&self) -> &ReferenceFrame {
&self.reference_frame
}
#[must_use]
pub fn position(&self) -> ECEF {
self.position
}
#[must_use]
pub fn velocity(&self) -> Option<ECEF> {
self.velocity
}
#[must_use]
pub fn epoch(&self) -> GpsTime {
self.epoch
}
#[must_use]
pub fn adjust_epoch(self, new_epoch: &GpsTime) -> Self {
let dt =
new_epoch.to_fractional_year_hardcoded() - self.epoch.to_fractional_year_hardcoded();
let v = self.velocity.unwrap_or_default();
Coordinate {
position: self.position + dt * v,
velocity: self.velocity,
epoch: *new_epoch,
reference_frame: self.reference_frame,
}
}
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.velocity {
Some(v) => write!(
f,
"Coordinate {{ ref_frame: {}, pos: {}, vel: {}, epoch: wn={} tow={} }}",
self.reference_frame,
self.position,
v,
self.epoch.wn(),
self.epoch.tow()
),
None => write!(
f,
"Coordinate {{ ref_frame: {}, pos: {}, vel: None, epoch: wn={} tow={} }}",
self.reference_frame,
self.position,
self.epoch.wn(),
self.epoch.tow()
),
}
}
}
#[cfg(test)]
mod tests {
use float_eq::assert_float_eq;
use proptest::prelude::*;
use super::*;
use crate::time::UtcTime;
#[test]
fn display_azimuth_elevation() {
let azel = AzimuthElevation::new(1.2, -2.1);
assert_eq!(format!("{azel}"), "AzimuthElevation { az: 1.2, el: -2.1 }");
}
#[test]
fn display_coordinate_with_velocity() {
let epoch = UtcTime::from_parts(2020, 1, 1, 0, 0, 0.).to_gps_hardcoded();
let coord = Coordinate::with_velocity(
ReferenceFrame::ITRF2014,
ECEF::new(1.0, 2.0, 3.0),
ECEF::new(4.0, 5.0, 6.0),
epoch,
);
assert_eq!(
format!("{coord}"),
format!(
"Coordinate {{ ref_frame: ITRF2014, pos: ECEF {{ x: 1, y: 2, z: 3 }}, vel: ECEF {{ x: 4, y: 5, z: 6 }}, epoch: wn={} tow={} }}",
epoch.wn(),
epoch.tow()
)
);
}
#[test]
fn display_coordinate() {
let epoch = UtcTime::from_parts(2020, 1, 1, 23, 2, 4.0).to_gps_hardcoded();
let coord = Coordinate::without_velocity(
ReferenceFrame::ITRF2020,
ECEF::new(-1.5, 2.78, 3.0),
epoch,
);
assert_eq!(
format!("{coord}"),
format!(
"Coordinate {{ ref_frame: ITRF2020, pos: ECEF {{ x: -1.5, y: 2.78, z: 3 }}, vel: None, epoch: wn={} tow={} }}",
epoch.wn(),
epoch.tow()
)
);
}
const MAX_DIST_ERROR_M: f64 = 1e-6;
const MAX_ANGLE_ERROR_SECS: f64 = 1e-7;
const MAX_ANGLE_ERROR_RAD: f64 = (MAX_ANGLE_ERROR_SECS / 3600.0).to_radians();
#[expect(clippy::float_cmp)]
#[test]
fn llhrad2deg() {
let zeros = LLHRadians::default();
let deg = zeros.to_degrees();
assert_eq!(0.0, deg.latitude());
assert_eq!(0.0, deg.longitude());
assert_eq!(0.0, deg.height());
let swift_home: LLHDegrees = [37.779_804, -122.391_751, 60.0].into();
let rads = swift_home.to_radians();
assert!((rads.latitude() - 0.659_381_970_558).abs() < MAX_ANGLE_ERROR_RAD);
assert!((rads.longitude() + 2.136_139_032_231).abs() < MAX_ANGLE_ERROR_RAD);
assert!(
rads.height() == swift_home.height(),
"rads.height() = {}, swift_home.height() = {}",
rads.height(),
swift_home.height()
);
}
const LLH_VALUES: [[f64; 3]; 10] = [
[0.0, 0.0, 0.0],
[0.0, 180.0_f64.to_radians(), 0.0],
[0.0, 90.0_f64.to_radians(), 0.0],
[0.0, -90.0_f64.to_radians(), 0.0],
[90.0_f64.to_radians(), 0.0, 0.0],
[-90.0_f64.to_radians(), 0.0, 0.0],
[90.0_f64.to_radians(), 0.0, 22.0],
[-90.0_f64.to_radians(), 0.0, 22.0],
[0.0, 0.0, 22.0],
[0.0, 180.0_f64.to_radians(), 22.0],
];
const EARTH_A: f64 = 6_378_137.0;
const EARTH_B: f64 = 6_356_752.314_245_179;
const ECEF_VALUES: [[f64; 3]; 10] = [
[EARTH_A, 0.0, 0.0],
[-EARTH_A, 0.0, 0.0],
[0.0, EARTH_A, 0.0],
[0.0, -EARTH_A, 0.0],
[0.0, 0.0, EARTH_B],
[0.0, 0.0, -EARTH_B],
[0.0, 0.0, (EARTH_B + 22.0)],
[0.0, 0.0, -(EARTH_B + 22.0)],
[(22.0 + EARTH_A), 0.0, 0.0],
[-(22.0 + EARTH_A), 0.0, 0.0],
];
#[test]
fn llh2ecef() {
for (llh_input, expected_ecef) in LLH_VALUES.iter().zip(ECEF_VALUES.iter()) {
let llh_input: LLHRadians = llh_input.into();
let expected_ecef: ECEF = expected_ecef.into();
let ecef = llh_input.to_ecef();
assert!(!ecef.x().is_nan());
assert!(!ecef.y().is_nan());
assert!(!ecef.z().is_nan());
let x_err = ecef.x() - expected_ecef.x();
assert!(x_err.abs() < MAX_DIST_ERROR_M);
let y_err = ecef.y() - expected_ecef.y();
assert!(y_err.abs() < MAX_DIST_ERROR_M);
let z_err = ecef.z() - expected_ecef.z();
assert!(z_err.abs() < MAX_DIST_ERROR_M);
}
}
#[test]
fn ecef2llh() {
for (ecef_input, expected_llh) in ECEF_VALUES.iter().zip(LLH_VALUES.iter()) {
let ecef_input: ECEF = ecef_input.into();
let expected_llh: LLHRadians = expected_llh.into();
let llh = ecef_input.to_llh();
assert!(!llh.latitude().is_nan());
assert!(!llh.longitude().is_nan());
assert!(!llh.height().is_nan());
let lat_err = llh.latitude() - expected_llh.latitude();
assert!(lat_err.abs() < MAX_ANGLE_ERROR_RAD);
let lon_err = llh.longitude() - expected_llh.longitude();
assert!(lon_err.abs() < MAX_ANGLE_ERROR_RAD);
let height_err = llh.height() - expected_llh.height();
assert!(height_err.abs() < MAX_DIST_ERROR_M);
}
}
#[test]
fn llh2ecef2llh() {
for llh_input in &LLH_VALUES {
let llh_input: LLHRadians = llh_input.into();
let llh_output = llh_input.to_ecef().to_llh();
assert!(!llh_output.latitude().is_nan());
assert!(!llh_output.longitude().is_nan());
assert!(!llh_output.height().is_nan());
let lat_err = llh_input.latitude() - llh_output.latitude();
assert!(lat_err.abs() < MAX_ANGLE_ERROR_RAD);
let lon_err = llh_input.longitude() - llh_output.longitude();
assert!(lon_err.abs() < MAX_ANGLE_ERROR_RAD);
let hgt_err = llh_input.height() - llh_output.height();
assert!(hgt_err.abs() < MAX_DIST_ERROR_M);
}
}
#[test]
fn ecef2llh2ecef() {
for ecef_input in &ECEF_VALUES {
let ecef_input: ECEF = ecef_input.into();
let ecef_output = ecef_input.to_llh().to_ecef();
assert!(!ecef_output.x().is_nan());
assert!(!ecef_output.y().is_nan());
assert!(!ecef_output.z().is_nan());
let x_err = ecef_input.x() - ecef_output.x();
assert!(x_err.abs() < MAX_DIST_ERROR_M);
let y_err = ecef_input.y() - ecef_output.y();
assert!(y_err.abs() < MAX_DIST_ERROR_M);
let z_err = ecef_input.z() - ecef_output.z();
assert!(z_err.abs() < MAX_DIST_ERROR_M);
}
}
#[test]
fn ecef2ned() {
let ecef_position = LLHDegrees::new(0.0, 0.0, 0.0).to_ecef();
let ecef_vec = ECEF::new(1.0, 0.0, 0.0);
let ned_vec = ecef_vec.ned_vector_at(&ecef_position);
assert_float_eq!(ned_vec.n(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.e(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.d(), -1.0, abs <= MAX_DIST_ERROR_M);
let ecef_vec = ECEF::new(0.0, 1.0, 0.0);
let ned_vec = ecef_vec.ned_vector_at(&ecef_position);
assert_float_eq!(ned_vec.n(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.e(), 1.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.d(), 0.0, abs <= MAX_DIST_ERROR_M);
let ecef_vec = ECEF::new(0.0, 0.0, 1.0);
let ned_vec = ecef_vec.ned_vector_at(&ecef_position);
assert_float_eq!(ned_vec.n(), 1.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.e(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.d(), 0.0, abs <= MAX_DIST_ERROR_M);
let ecef_position = LLHDegrees::new(90.0, 0.0, 0.0).to_ecef();
let ecef_vec = ECEF::new(1.0, 0.0, 0.0);
let ned_vec = ecef_vec.ned_vector_at(&ecef_position);
assert_float_eq!(ned_vec.n(), -1.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.e(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.d(), 0.0, abs <= MAX_DIST_ERROR_M);
let ecef_vec = ECEF::new(0.0, 1.0, 0.0);
let ned_vec = ecef_vec.ned_vector_at(&ecef_position);
assert_float_eq!(ned_vec.n(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.e(), 1.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.d(), 0.0, abs <= MAX_DIST_ERROR_M);
let ecef_vec = ECEF::new(0.0, 0.0, 1.0);
let ned_vec = ecef_vec.ned_vector_at(&ecef_position);
assert_float_eq!(ned_vec.n(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.e(), 0.0, abs <= MAX_DIST_ERROR_M);
assert_float_eq!(ned_vec.d(), -1.0, abs <= MAX_DIST_ERROR_M);
}
#[test]
fn coordinate_epoch() {
let initial_epoch = UtcTime::from_parts(2020, 1, 1, 0, 0, 0.).to_gps_hardcoded();
let new_epoch = UtcTime::from_parts(2021, 1, 1, 0, 0, 0.).to_gps_hardcoded();
let initial_coord = Coordinate::with_velocity(
ReferenceFrame::ITRF2020,
ECEF::new(0.0, 0.0, 0.0),
ECEF::new(1.0, 2.0, 3.0),
initial_epoch,
);
let new_coord = initial_coord.clone().adjust_epoch(&new_epoch);
assert_eq!(initial_coord.reference_frame, new_coord.reference_frame);
assert_float_eq!(new_coord.position.x(), 1.0, abs <= 0.001);
assert_float_eq!(new_coord.position.y(), 2.0, abs <= 0.001);
assert_float_eq!(new_coord.position.z(), 3.0, abs <= 0.001);
assert_float_eq!(new_coord.velocity.unwrap().x(), 1.0, abs <= 0.001);
assert_float_eq!(new_coord.velocity.unwrap().y(), 2.0, abs <= 0.001);
assert_float_eq!(new_coord.velocity.unwrap().z(), 3.0, abs <= 0.001);
assert_eq!(new_epoch, new_coord.epoch());
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn prop_llh2ecef2llh_identity(lat in -90.0..90.0, lon in -180.0..180.0, height in (-0.5*EARTH_A)..(4.0*EARTH_A)) {
let llh_input = LLHDegrees::new(lat, lon, height).to_radians();
let ecef = llh_input.to_ecef();
let llh_output = ecef.to_llh();
assert!(!llh_output.latitude().is_nan());
assert!(!llh_output.longitude().is_nan());
assert!(!llh_output.height().is_nan());
let lat_err = llh_input.latitude() - llh_output.latitude();
assert!(lat_err.abs() < MAX_ANGLE_ERROR_RAD,
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {llh_input:?}, ECEF: {ecef:?}, Final: {llh_output:?}, Lat error (rad): {lat_err}");
let lon_err = llh_input.longitude() - llh_output.longitude();
assert!(lon_err.abs() < MAX_ANGLE_ERROR_RAD,
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {llh_input:?}, ECEF: {ecef:?}, Final: {llh_output:?}, Lon error (rad): {lon_err}");
let hgt_err = llh_input.height() - llh_output.height();
assert!(hgt_err.abs() < MAX_DIST_ERROR_M,
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {:?}, ECEF: {:?}, Final: {:?}, Height error (mm): {}", llh_input, ecef, llh_output, hgt_err*1000.0);
}
#[test]
fn prop_ecef2llh2ecef_identity(x in (-4.0*EARTH_A)..(4.0*EARTH_A), y in (-4.0*EARTH_A)..(4.0*EARTH_A), z in (-4.0*EARTH_A)..(4.0*EARTH_A)) {
prop_assume!((x*x + y*y + z*z).sqrt().abs() > 0.5*EARTH_A);
let ecef_input = ECEF::new(x, y, z);
let llh = ecef_input.to_llh();
let ecef_output = llh.to_ecef();
assert!(!ecef_output.x().is_nan());
assert!(!ecef_output.y().is_nan());
assert!(!ecef_output.z().is_nan());
let x_err = ecef_input.x() - ecef_output.x();
assert!(x_err.abs() < MAX_DIST_ERROR_M,
"Converting random WGS84 ECEF to LLH and back again does not return the original values. Initial: {:?}, LLH: {:?}, Final: {:?}, X error (mm): {}", ecef_input, llh.to_degrees(), ecef_output, x_err*1000.0);
let y_err = ecef_input.y() - ecef_output.y();
assert!(y_err.abs() < MAX_DIST_ERROR_M,
"Converting random WGS84 ECEF to LLH and back again does not return the original values. Initial: {:?}, LLH: {:?}, Final: {:?}, Y error (mm): {}", ecef_input, llh.to_degrees(), ecef_output, y_err*1000.0);
let z_err = ecef_input.z() - ecef_output.z();
assert!(z_err.abs() < MAX_DIST_ERROR_M,
"Converting random WGS84 ECEF to LLH and back again does not return the original values. Initial: {:?}, LLH: {:?}, Final: {:?}, Z error (mm): {}", ecef_input, llh.to_degrees(), ecef_output, z_err*1000.0);
}
#[test]
fn prop_ecef2ned_identity(x in -1e8..1e8, y in -1e8..1e8, z in -1e8..1e8) {
let ecef = ECEF::new(x, y, z);
let ned_output = ecef.ned_to(&ecef);
assert!(!ned_output.n().is_nan());
assert!(!ned_output.e().is_nan());
assert!(!ned_output.d().is_nan());
assert!(ned_output.n().abs() < 1e-8,
"NED vector to reference ECEF point has nonzero element north: {} mm (point was {:?})",
ned_output.n()*1000.0,
ecef);
assert!(ned_output.e().abs() < 1e-8,
"NED vector to reference ECEF point has nonzero element east: {} mm (point was {:?})",
ned_output.e()*1000.0,
ecef);
assert!(ned_output.d().abs() < 1e-8,
"NED vector to reference ECEF point has nonzero element down: {} mm (point was {:?})",
ned_output.d()*1000.0,
ecef);
}
}
}