use core::fmt;
use core::marker::PhantomData;
use core::ops::{Add, Div, Mul, Neg, Sub};
use crate::angle::TrueCourse;
use crate::error::Result;
use crate::geodesy::{EcefPoint, Ellipsoid, GeodeticPoint};
use crate::math;
use crate::units::{Distance, Speed};
mod sealed {
pub trait Sealed {}
}
pub trait VectorFrame:
sealed::Sealed + Copy + Clone + fmt::Debug + Eq + core::hash::Hash + Default + 'static
{
const NAME: &'static str;
const AXES: [&'static str; 3];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ned;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Enu;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Body;
impl sealed::Sealed for Ned {}
impl sealed::Sealed for Enu {}
impl sealed::Sealed for Body {}
impl VectorFrame for Ned {
const NAME: &'static str = "NED";
const AXES: [&'static str; 3] = ["north", "east", "down"];
}
impl VectorFrame for Enu {
const NAME: &'static str = "ENU";
const AXES: [&'static str; 3] = ["east", "north", "up"];
}
impl VectorFrame for Body {
const NAME: &'static str = "body";
const AXES: [&'static str; 3] = ["forward", "right", "down"];
}
pub trait VectorUnit:
sealed::Sealed
+ Copy
+ fmt::Debug
+ PartialEq
+ Add<Output = Self>
+ Sub<Output = Self>
+ Neg<Output = Self>
+ Mul<f64, Output = Self>
{
fn si(self) -> f64;
fn from_si(value: f64) -> Self;
}
impl sealed::Sealed for Distance {}
impl sealed::Sealed for Speed {}
impl VectorUnit for Distance {
fn si(self) -> f64 {
self.metres()
}
fn from_si(value: f64) -> Self {
Self::from_metres(value).unwrap_or(Self::ZERO)
}
}
impl VectorUnit for Speed {
fn si(self) -> f64 {
self.metres_per_second()
}
fn from_si(value: f64) -> Self {
Self::from_metres_per_second(value).unwrap_or(Self::ZERO)
}
}
#[derive(Clone, Copy, PartialEq)]
pub struct Vector3<F: VectorFrame, U: VectorUnit> {
components: [U; 3],
frame: PhantomData<F>,
}
impl<F: VectorFrame, U: VectorUnit> Vector3<F, U> {
#[must_use]
pub const fn new(first: U, second: U, third: U) -> Self {
Self {
components: [first, second, third],
frame: PhantomData,
}
}
#[must_use]
pub const fn components(&self) -> [U; 3] {
self.components
}
#[must_use]
pub fn magnitude(&self) -> U {
let [a, b, c] = self.si();
U::from_si(math::hypot(math::hypot(a, b), c))
}
#[must_use]
pub fn horizontal_magnitude(&self) -> U {
let [a, b, _] = self.si();
U::from_si(math::hypot(a, b))
}
fn si(&self) -> [f64; 3] {
self.components.map(U::si)
}
fn from_si(components: [f64; 3]) -> Self {
Self {
components: components.map(U::from_si),
frame: PhantomData,
}
}
}
impl<U: VectorUnit> Vector3<Ned, U> {
#[must_use]
pub const fn north(&self) -> U {
self.components[0]
}
#[must_use]
pub const fn east(&self) -> U {
self.components[1]
}
#[must_use]
pub const fn down(&self) -> U {
self.components[2]
}
#[must_use]
pub fn to_enu(self) -> Vector3<Enu, U> {
Vector3::new(self.east(), self.north(), -self.down())
}
#[must_use]
pub fn horizontal_direction(&self) -> Option<TrueCourse> {
horizontal_direction(self.north().si(), self.east().si())
}
}
impl<U: VectorUnit> Vector3<Enu, U> {
#[must_use]
pub const fn east(&self) -> U {
self.components[0]
}
#[must_use]
pub const fn north(&self) -> U {
self.components[1]
}
#[must_use]
pub const fn up(&self) -> U {
self.components[2]
}
#[must_use]
pub fn to_ned(self) -> Vector3<Ned, U> {
Vector3::new(self.north(), self.east(), -self.up())
}
#[must_use]
pub fn horizontal_direction(&self) -> Option<TrueCourse> {
horizontal_direction(self.north().si(), self.east().si())
}
}
impl<U: VectorUnit> Vector3<Body, U> {
#[must_use]
pub const fn forward(&self) -> U {
self.components[0]
}
#[must_use]
pub const fn right(&self) -> U {
self.components[1]
}
#[must_use]
pub const fn down(&self) -> U {
self.components[2]
}
}
fn horizontal_direction(north: f64, east: f64) -> Option<TrueCourse> {
let scale = math::abs(north).max(math::abs(east));
if scale < f64::MIN_POSITIVE {
return None;
}
TrueCourse::wrap(math::to_degrees(math::atan2(east, north))).ok()
}
impl<F: VectorFrame, U: VectorUnit> Add for Vector3<F, U> {
type Output = Self;
fn add(self, other: Self) -> Self {
let [first, second, third] = self.components;
let [x, y, z] = other.components;
Self::new(first + x, second + y, third + z)
}
}
impl<F: VectorFrame, U: VectorUnit> Sub for Vector3<F, U> {
type Output = Self;
fn sub(self, other: Self) -> Self {
let [first, second, third] = self.components;
let [x, y, z] = other.components;
Self::new(first - x, second - y, third - z)
}
}
impl<F: VectorFrame, U: VectorUnit> Neg for Vector3<F, U> {
type Output = Self;
fn neg(self) -> Self {
let [a, b, c] = self.components;
Self::new(-a, -b, -c)
}
}
impl<F: VectorFrame, U: VectorUnit> Mul<f64> for Vector3<F, U> {
type Output = Self;
fn mul(self, factor: f64) -> Self {
let [a, b, c] = self.components;
Self::new(a * factor, b * factor, c * factor)
}
}
impl<F: VectorFrame, U: VectorUnit> Div<f64> for Vector3<F, U> {
type Output = Self;
fn div(self, divisor: f64) -> Self {
Self::from_si(self.si().map(|value| value / divisor))
}
}
impl<F: VectorFrame, U: VectorUnit> fmt::Debug for Vector3<F, U> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct(F::NAME);
for (axis, component) in F::AXES.iter().zip(&self.components) {
debug.field(axis, component);
}
debug.finish()
}
}
impl<F: VectorFrame, U: VectorUnit + fmt::Display> fmt::Display for Vector3<F, U> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("(")?;
for (index, (axis, component)) in F::AXES.iter().zip(&self.components).enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{axis} {component}")?;
}
f.write_str(")")
}
}
#[cfg(feature = "serde")]
impl<F: VectorFrame, U: VectorUnit + serde::Serialize> serde::Serialize for Vector3<F, U> {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> core::result::Result<S::Ok, S::Error> {
self.components.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, F: VectorFrame, U: VectorUnit + serde::Deserialize<'de>> serde::Deserialize<'de>
for Vector3<F, U>
{
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> core::result::Result<Self, D::Error> {
let [a, b, c] = <[U; 3]>::deserialize(deserializer)?;
Ok(Self::new(a, b, c))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LocalFrame {
origin: GeodeticPoint,
origin_ecef: EcefPoint,
ellipsoid: Ellipsoid,
rotation: [[f64; 3]; 3],
}
impl LocalFrame {
pub fn at(origin: GeodeticPoint, ellipsoid: &Ellipsoid) -> Result<Self> {
let origin_ecef = EcefPoint::from_geodetic(origin, ellipsoid)?;
let (sin_lat, cos_lat) = sin_cos(origin.position().latitude().radians());
let (sin_lon, cos_lon) = sin_cos(origin.position().longitude().radians());
Ok(Self {
origin,
origin_ecef,
ellipsoid: *ellipsoid,
rotation: [
[-sin_lat * cos_lon, -sin_lat * sin_lon, cos_lat],
[-sin_lon, cos_lon, 0.0],
[-cos_lat * cos_lon, -cos_lat * sin_lon, -sin_lat],
],
})
}
#[must_use]
pub const fn origin(&self) -> GeodeticPoint {
self.origin
}
#[must_use]
pub const fn ellipsoid(&self) -> &Ellipsoid {
&self.ellipsoid
}
pub fn ned_of(&self, point: GeodeticPoint) -> Result<Vector3<Ned, Distance>> {
let ecef = EcefPoint::from_geodetic(point, &self.ellipsoid)?;
let delta = [
ecef.x().metres() - self.origin_ecef.x().metres(),
ecef.y().metres() - self.origin_ecef.y().metres(),
ecef.z().metres() - self.origin_ecef.z().metres(),
];
Ok(Vector3::from_si(self.rotation.map(|row| dot(row, delta))))
}
pub fn enu_of(&self, point: GeodeticPoint) -> Result<Vector3<Enu, Distance>> {
self.ned_of(point).map(Vector3::to_enu)
}
pub fn point_from_ned(&self, displacement: Vector3<Ned, Distance>) -> Result<GeodeticPoint> {
let local = displacement.si();
let column = |index: usize| {
self.rotation
.iter()
.zip(local)
.map(|(row, value)| row.get(index).copied().unwrap_or(0.0) * value)
.sum::<f64>()
};
let ecef = EcefPoint::new(
Distance::from_si(self.origin_ecef.x().metres() + column(0)),
Distance::from_si(self.origin_ecef.y().metres() + column(1)),
Distance::from_si(self.origin_ecef.z().metres() + column(2)),
);
ecef.to_geodetic(&self.ellipsoid)
}
pub fn point_from_enu(&self, displacement: Vector3<Enu, Distance>) -> Result<GeodeticPoint> {
self.point_from_ned(displacement.to_ned())
}
}
fn sin_cos(radians: f64) -> (f64, f64) {
(math::sin(radians), math::cos(radians))
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use crate::geodesy::Height;
use crate::position::{Latitude, Longitude, Position};
use alloc::format;
fn metres(value: f64) -> Distance {
Distance::from_metres(value).unwrap()
}
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(metres(height)),
)
}
#[test]
fn vectors_add_scale_and_measure_within_one_frame_and_unit() {
let a: Vector3<Ned, Distance> = Vector3::new(metres(3.0), metres(4.0), metres(12.0));
let b = Vector3::new(metres(1.0), metres(1.0), metres(1.0));
let close = |vector: Vector3<Ned, Distance>, wanted: [f64; 3]| {
vector
.components()
.iter()
.zip(wanted)
.all(|(got, wanted)| (got.metres() - wanted).abs() < 1e-9)
};
assert!(close(a + b, [4.0, 5.0, 13.0]));
assert!(close(a - b, [2.0, 3.0, 11.0]));
assert!(close(-a, [-3.0, -4.0, -12.0]));
assert!(close(a * 2.0, [6.0, 8.0, 24.0]));
assert!(close(a / 2.0, [1.5, 2.0, 6.0]));
assert!((a.magnitude().metres() - 13.0).abs() < 1e-9);
assert!((a.horizontal_magnitude().metres() - 5.0).abs() < 1e-9);
let printed = format!("{a:?}");
assert!(printed.starts_with("NED { north: "), "{printed}");
assert!(format!("{a}").starts_with("(north "), "{a}");
}
#[test]
fn ned_and_enu_are_the_same_vector_written_differently() {
let ned: Vector3<Ned, Speed> = Vector3::new(
Speed::from_metres_per_second(4.0).unwrap(),
Speed::from_metres_per_second(1.0).unwrap(),
Speed::from_metres_per_second(-0.5).unwrap(),
);
let enu = ned.to_enu();
assert_eq!(enu.east(), ned.east());
assert_eq!(enu.north(), ned.north());
assert_eq!(enu.up().metres_per_second(), 0.5);
assert_eq!(enu.to_ned(), ned);
let course = ned.horizontal_direction().unwrap();
assert!((course.degrees() - 14.036_243_467_926_479).abs() < 1e-9);
assert_eq!(enu.horizontal_direction(), Some(course));
let still: Vector3<Ned, Speed> = Vector3::new(Speed::ZERO, Speed::ZERO, Speed::ZERO);
assert_eq!(still.horizontal_direction(), None);
}
#[test]
fn a_local_frame_measures_displacements_from_its_origin() {
let frame = LocalFrame::at(point(50.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
let zero = frame.ned_of(point(50.0, 0.0, 0.0)).unwrap();
assert!(zero.magnitude().metres() < 1e-6);
let above = frame.ned_of(point(50.0, 0.0, 100.0)).unwrap();
assert!(above.north().metres().abs() < 1e-6);
assert!(above.east().metres().abs() < 1e-6);
assert!((above.down().metres() + 100.0).abs() < 1e-6);
let east = frame.ned_of(point(50.0, 0.01, 0.0)).unwrap();
assert!(east.east().metres() > 700.0 && east.east().metres() < 720.0);
assert!(east.north().metres().abs() < 0.1);
assert!(east.down().metres() > 0.0);
assert_eq!(east.horizontal_direction().unwrap().degrees().round(), 90.0);
}
#[test]
fn displacements_round_trip_through_the_frame() {
let origins = [
point(50.0, 0.0, 0.0),
point(89.99, 179.99, 10.0),
point(-33.9, 151.2, 50.0),
point(0.0, -180.0, -30.0),
];
for origin in origins {
let frame = LocalFrame::at(origin, &Ellipsoid::WGS84).unwrap();
let displacement: Vector3<Ned, Distance> =
Vector3::new(metres(12_345.6), metres(-9_876.5), metres(432.1));
let there = frame.point_from_ned(displacement).unwrap();
let back = frame.ned_of(there).unwrap();
let error = (back - displacement).magnitude().metres();
assert!(error < 1e-4, "{origin}: off by {error} m, {back:?}");
let enu_back = frame.enu_of(there).unwrap();
let again = frame.point_from_enu(enu_back).unwrap();
assert!(
(again.position().latitude().degrees() - there.position().latitude().degrees())
.abs()
< 1e-9
);
}
}
#[test]
fn the_frame_wants_an_ellipsoidal_origin() {
let msl = GeodeticPoint::new(
point(50.0, 0.0, 0.0).position(),
Height::above_mean_sea_level(Distance::ZERO),
);
assert!(LocalFrame::at(msl, &Ellipsoid::WGS84).is_err());
}
}