#[cfg(all(feature = "libm", not(feature = "std"), not(test)))]
use crate::math::Float as _;
use crate::vector::{vector_ops, Vec3};
pub(crate) fn wrap_360(deg: f64) -> f64 {
let b = if (-360.0..0.0).contains(°) {
deg + 360.0 } else if (0.0..360.0).contains(°) {
deg
} else {
deg.rem_euclid(360.0)
};
if b >= 360.0 {
0.0
} else {
b
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Enu {
pub east: f64,
pub north: f64,
pub up: f64,
}
impl Enu {
#[inline]
pub const fn new(east: f64, north: f64, up: f64) -> Self {
Enu { east, north, up }
}
#[inline]
pub fn norm(self) -> f64 {
self.vec().norm()
}
#[inline]
pub const fn vec(self) -> Vec3 {
Vec3::new(self.east, self.north, self.up)
}
#[inline]
pub const fn to_ned(self) -> Ned {
Ned::new(self.north, self.east, -self.up)
}
#[inline]
pub fn to_aer(self) -> Aer {
Aer::from_enu(self)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ned {
pub north: f64,
pub east: f64,
pub down: f64,
}
impl Ned {
#[inline]
pub const fn new(north: f64, east: f64, down: f64) -> Self {
Ned { north, east, down }
}
#[inline]
pub fn norm(self) -> f64 {
self.to_enu().norm()
}
#[inline]
pub const fn vec(self) -> Vec3 {
Vec3::new(self.north, self.east, self.down)
}
#[inline]
pub const fn to_enu(self) -> Enu {
Enu::new(self.east, self.north, -self.down)
}
#[inline]
pub fn to_aer(self) -> Aer {
Aer::from_enu(self.to_enu())
}
}
vector_ops!(Enu { east, north, up });
vector_ops!(Ned { north, east, down });
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Aer {
pub azimuth_deg: f64,
pub elevation_deg: f64,
pub range_m: f64,
}
impl Aer {
#[inline]
pub const fn new(azimuth_deg: f64, elevation_deg: f64, range_m: f64) -> Self {
Aer {
azimuth_deg,
elevation_deg,
range_m,
}
}
#[doc(alias = "enu2aer")]
#[inline]
pub fn from_enu(v: Enu) -> Aer {
let azimuth_deg = wrap_360(v.east.atan2(v.north).to_degrees());
let elevation_deg = v.up.atan2((v.east * v.east + v.north * v.north).sqrt()).to_degrees();
Aer {
azimuth_deg,
elevation_deg,
range_m: v.norm(),
}
}
#[inline]
pub fn from_ned(v: Ned) -> Aer {
Aer::from_enu(v.to_enu())
}
#[doc(alias = "aer2enu")]
#[inline]
pub fn to_enu(self) -> Enu {
let d = self.direction_enu();
Enu::new(d.east * self.range_m, d.north * self.range_m, d.up * self.range_m)
}
#[inline]
pub fn to_ned(self) -> Ned {
self.to_enu().to_ned()
}
#[inline]
pub fn direction_enu(self) -> Enu {
let (saz, caz) = self.azimuth_deg.to_radians().sin_cos();
let (sel, cel) = self.elevation_deg.to_radians().sin_cos();
Enu::new(saz * cel, caz * cel, sel)
}
#[inline]
pub fn is_above_horizon(self) -> bool {
self.elevation_deg > 0.0
}
#[inline]
pub fn normalized(self) -> Aer {
Aer {
azimuth_deg: wrap_360(self.azimuth_deg),
..self
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::close;
#[test]
fn local_vector_operators() {
let e = Enu::new(1.0, 2.0, 3.0);
assert_eq!(e + e, Enu::new(2.0, 4.0, 6.0));
assert_eq!(e - e, Enu::default());
assert_eq!(e * 2.0, Enu::new(2.0, 4.0, 6.0));
assert_eq!(-e, Enu::new(-1.0, -2.0, -3.0));
assert_eq!(Enu::from([1.0, 2.0, 3.0]), e);
assert_eq!(<[f64; 3]>::from(e), [1.0, 2.0, 3.0]);
let n = e.to_ned();
assert_eq!(n, Ned::new(2.0, 1.0, -3.0));
assert_eq!(n + n, Ned::new(4.0, 2.0, -6.0));
assert_eq!(n.vec(), Vec3::new(2.0, 1.0, -3.0));
assert_eq!(n.to_enu(), e);
assert!(close(n.norm(), e.norm(), 1e-12));
}
#[test]
fn elevation_keeps_its_precision_near_the_zenith() {
let el = Enu::new(1e-9, 0.0, 1.0).to_aer().elevation_deg;
assert!(close(el, 90.0 - 1e-9_f64.to_degrees(), 1e-13), "{el}");
assert_eq!(Enu::new(0.0, 0.0, -5.0).to_aer().elevation_deg, -90.0);
}
#[test]
fn aer_ned_and_horizon() {
let aer = Aer::new(45.0, 10.0, 1000.0);
let back = Aer::from_ned(aer.to_ned());
assert!(
close(back.azimuth_deg, 45.0, 1e-9)
&& close(back.elevation_deg, 10.0, 1e-9)
&& close(back.range_m, 1000.0, 1e-9)
);
assert_eq!(aer.to_ned().to_aer().normalized().azimuth_deg.round(), 45.0);
assert!(aer.is_above_horizon() && !Aer::new(0.0, -1.0, 1.0).is_above_horizon());
assert_eq!(Aer::new(-90.0, 0.0, 1.0).normalized().azimuth_deg, 270.0);
assert_eq!(Aer::from_enu(Enu::default()), Aer::new(0.0, 0.0, 0.0));
assert!(close(Aer::new(90.0, 0.0, 1_000.0).to_enu().east, 1_000.0, 1e-9));
assert_eq!(Aer::from_enu(Enu::new(-1e-30, 1.0, 0.0)).azimuth_deg, 0.0);
assert_eq!(wrap_360(-1e-20), 0.0);
assert_eq!(wrap_360(-90.0), 270.0);
assert_eq!(wrap_360(725.0), 5.0);
assert!(wrap_360(f64::NAN).is_nan());
}
}