use crate::builder::{Set, Unset};
use crate::coordinate_systems::{
CoordinateSystem, EnuLike, FrdLike, HasComponents, NedLike, RightHandedXyzLike,
};
use crate::directions::Bearing;
use crate::float_math::FloatMath;
use crate::math::RigidBodyTransform;
use crate::systems::EquivalentTo;
use crate::vectors::Vector;
use crate::{Point3, engineering};
use core::fmt;
use core::fmt::{Display, Formatter};
use core::marker::PhantomData;
use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
use uom::ConstZero;
use uom::si::angle::radian;
use uom::si::f64::{Angle, Length};
use uom::si::length::meter;
use uom::si::ratio::ratio;
#[cfg(doc)]
use crate::{engineering::Pose, systems::BearingDefined};
#[cfg(any(test, feature = "approx"))]
use approx::{AbsDiffEq, RelativeEq};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(bound = ""))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Coordinate<In> {
pub(crate) point: Point3,
#[cfg_attr(feature = "serde", serde(skip))]
system: PhantomData<In>,
}
impl<In> Clone for Coordinate<In> {
fn clone(&self) -> Self {
*self
}
}
impl<In> Copy for Coordinate<In> {}
#[macro_export]
macro_rules! coordinate {
($x:tt = $xx:expr, $y:tt = $yy:expr, $z:tt = $zz:expr $(,)?) => {
coordinate!($x = $xx, $y = $yy, $z = $zz; in _)
};
(n = $n:expr, e = $e:expr, d = $d:expr; in $in:ty) => {
$crate::Coordinate::<$in>::build($crate::systems::NedComponents {
north: $n.into(),
east: $e.into(),
down: $d.into(),
})
};
(e = $e:expr, n = $n:expr, u = $u:expr; in $in:ty) => {
$crate::Coordinate::<$in>::build($crate::systems::EnuComponents {
east: $e.into(),
north: $n.into(),
up: $u.into(),
})
};
(f = $f:expr, r = $r:expr, d = $d:expr; in $in:ty) => {
$crate::Coordinate::<$in>::build($crate::systems::FrdComponents {
front: $f.into(),
right: $r.into(),
down: $d.into(),
})
};
(x = $x:expr, y = $y:expr, z = $z:expr; in $in:ty) => {
$crate::Coordinate::<$in>::build($crate::systems::XyzComponents {
x: $x.into(),
y: $y.into(),
z: $z.into(),
})
};
}
impl<In> Coordinate<In> {
pub(crate) fn from_nalgebra_point(p: Point3) -> Self {
Self {
point: p,
system: PhantomData,
}
}
#[must_use]
pub fn build(components: <In::Convention as HasComponents>::Components) -> Self
where
In: CoordinateSystem,
In::Convention: HasComponents,
{
let [x, y, z] = components.into();
#[allow(deprecated)]
Self::from_cartesian(x, y, z)
}
pub fn builder() -> Builder<In, Unset, Unset, Unset>
where
In: CoordinateSystem,
{
Builder::default()
}
#[deprecated = "prefer `Coordinate::builder` to avoid risk of argument order confusion"]
pub fn from_cartesian(
x: impl Into<Length>,
y: impl Into<Length>,
z: impl Into<Length>,
) -> Self {
Self::from_nalgebra_point(Point3::new(
x.into().get::<meter>(),
y.into().get::<meter>(),
z.into().get::<meter>(),
))
}
pub fn from_spherical(
radius: impl Into<Length>,
polar: impl Into<Angle>,
azimuth: impl Into<Angle>,
) -> Self {
let direction = Vector::from_spherical(radius, polar.into(), azimuth.into());
Self::origin() + direction
}
pub fn from_bearing(bearing: Bearing<In>, range: impl Into<Length>) -> Self
where
In: crate::systems::BearingDefined,
{
let direction = Vector::from_bearing(bearing, range);
Self::origin() + direction
}
#[must_use]
pub fn origin() -> Self {
Self {
point: Point3::origin(),
system: PhantomData,
}
}
#[doc(alias = "as_transform_to")]
#[doc(alias = "defines_pose_of")]
#[must_use]
pub unsafe fn map_as_zero_in<To>(self) -> RigidBodyTransform<In, To> {
unsafe {
engineering::Pose::new(self, engineering::Orientation::aligned()).map_as_zero_in()
}
}
#[must_use]
pub fn cast<NewIn>(self) -> Coordinate<NewIn>
where
In: EquivalentTo<NewIn>,
{
Coordinate {
point: self.point,
system: PhantomData::<NewIn>,
}
}
#[must_use]
pub fn lerp(&self, rhs: &Self, t: f64) -> Self {
Self {
point: self.point.lerp(&rhs.point, t),
system: self.system,
}
}
}
impl<In> Default for Coordinate<In> {
fn default() -> Self {
Self::origin()
}
}
macro_rules! accessors {
{
$convention:ident
using $x:ident, $y:ident, $z:ident
+ $x_ax:ident, $y_ax:ident, $z_ax:ident
} => {
impl<In> Coordinate<In> where In: CoordinateSystem<Convention = $convention> {
#[must_use]
pub fn $x(&self) -> Length { Length::new::<meter>(self.point.x) }
#[must_use]
pub fn $y(&self) -> Length { Length::new::<meter>(self.point.y) }
#[must_use]
pub fn $z(&self) -> Length { Length::new::<meter>(self.point.z) }
#[must_use]
pub fn $x_ax() -> Vector<In> { Vector::$x_ax() }
#[must_use]
pub fn $y_ax() -> Vector<In> { Vector::$y_ax() }
#[must_use]
pub fn $z_ax() -> Vector<In> { Vector::$z_ax() }
}
};
}
accessors!(RightHandedXyzLike using x, y, z + x_axis, y_axis, z_axis);
accessors!(NedLike using ned_north, ned_east, ned_down + ned_north_axis, ned_east_axis, ned_down_axis);
accessors!(FrdLike using frd_front, frd_right, frd_down + frd_front_axis, frd_right_axis, frd_down_axis);
accessors!(EnuLike using enu_east, enu_north, enu_up + enu_east_axis, enu_north_axis, enu_up_axis);
impl<In> Coordinate<In> {
#[doc(alias = "components")]
#[must_use]
pub fn to_cartesian(&self) -> [Length; 3] {
[
Length::new::<meter>(self.point.x),
Length::new::<meter>(self.point.y),
Length::new::<meter>(self.point.z),
]
}
#[doc(alias = "norm")]
#[must_use]
pub fn distance_from_origin(&self) -> Length {
Length::new::<meter>(self.point.coords.norm())
}
#[must_use]
pub fn distance_from(&self, other: &Self) -> Length {
(*other - *self).magnitude()
}
fn spherical_coordinates(&self) -> Option<(Angle, Angle)> {
let r = self.distance_from_origin();
if r == Length::ZERO {
return None;
}
let polar_value = (Length::new::<meter>(self.point.z) / r).get::<ratio>();
let polar = Angle::new::<radian>(FloatMath::acos(polar_value));
let xy = Length::new::<meter>(FloatMath::sqrt(
FloatMath::powi(self.point.x, 2) + FloatMath::powi(self.point.y, 2),
));
if xy == Length::ZERO {
return Some((polar, Angle::ZERO));
}
let azimuth_value = (Length::new::<meter>(self.point.x) / xy).get::<ratio>();
let azimuth = Angle::new::<radian>(FloatMath::copysign(
FloatMath::acos(azimuth_value),
self.point.y,
));
Some((polar, azimuth))
}
#[must_use]
pub fn bearing_from_origin(&self) -> Option<Bearing<In>>
where
In: crate::systems::BearingDefined,
{
let (polar, azimuth) = self.spherical_coordinates()?;
Some(
In::spherical_to_bearing(polar, azimuth)
.expect("polar is computed with acos, which is always 0°-180°"),
)
}
}
impl<In> PartialEq<Self> for Coordinate<In> {
fn eq(&self, other: &Self) -> bool {
self.point.eq(&other.point)
}
}
#[cfg(any(test, feature = "approx"))]
impl<In> AbsDiffEq<Self> for Coordinate<In> {
type Epsilon = Length;
fn default_epsilon() -> Self::Epsilon {
Length::new::<meter>(0.1)
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.point.abs_diff_eq(&other.point, epsilon.get::<meter>())
}
}
#[cfg(any(test, feature = "approx"))]
impl<In> RelativeEq for Coordinate<In> {
fn default_max_relative() -> Self::Epsilon {
Length::new::<meter>(Point3::default_max_relative())
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.point.relative_eq(
&other.point,
epsilon.get::<meter>(),
max_relative.get::<meter>(),
)
}
}
impl<In> Display for Coordinate<In> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.point)
}
}
impl<In> Neg for Coordinate<In> {
type Output = Self;
fn neg(self) -> Self::Output {
Self {
point: -self.point,
system: self.system,
}
}
}
impl<In> Sub<Self> for Coordinate<In> {
type Output = Vector<In>;
fn sub(self, rhs: Self) -> Self::Output {
Vector::from_nalgebra_vector(self.point - rhs.point)
}
}
impl<In> Add<Vector<In>> for Coordinate<In> {
type Output = Self;
fn add(self, rhs: Vector<In>) -> Self::Output {
Self {
point: self.point + rhs.inner,
system: self.system,
}
}
}
impl<In> AddAssign<Vector<In>> for Coordinate<In> {
fn add_assign(&mut self, rhs: Vector<In>) {
self.point += rhs.inner;
}
}
impl<In> Sub<Vector<In>> for Coordinate<In> {
type Output = Self;
fn sub(self, rhs: Vector<In>) -> Self::Output {
Self {
point: self.point - rhs.inner,
system: self.system,
}
}
}
impl<In> SubAssign<Vector<In>> for Coordinate<In> {
fn sub_assign(&mut self, rhs: Vector<In>) {
self.point -= rhs.inner;
}
}
#[derive(Debug)]
#[must_use]
pub struct Builder<In, X, Y, Z> {
under_construction: Coordinate<In>,
set: (PhantomData<X>, PhantomData<Y>, PhantomData<Z>),
}
impl<In> Default for Builder<In, Unset, Unset, Unset> {
fn default() -> Self {
Self {
under_construction: Coordinate::default(),
set: (PhantomData, PhantomData, PhantomData),
}
}
}
impl<In> Builder<In, Set, Set, Set> {
#[must_use]
pub fn build(self) -> Coordinate<In> {
self.under_construction
}
}
macro_rules! constructor {
($like:ident, [$x:ident, $y:ident, $z:ident]) => {
impl<In, X, Y, Z> Builder<In, X, Y, Z>
where
In: CoordinateSystem<Convention = $like>,
{
pub fn $x(mut self, length: impl Into<Length>) -> Builder<In, Set, Y, Z> {
self.under_construction.point.x = length.into().get::<meter>();
Builder {
under_construction: self.under_construction,
set: (PhantomData::<Set>, self.set.1, self.set.2),
}
}
pub fn $y(mut self, length: impl Into<Length>) -> Builder<In, X, Set, Z> {
self.under_construction.point.y = length.into().get::<meter>();
Builder {
under_construction: self.under_construction,
set: (self.set.0, PhantomData::<Set>, self.set.2),
}
}
pub fn $z(mut self, length: impl Into<Length>) -> Builder<In, X, Y, Set> {
self.under_construction.point.z = length.into().get::<meter>();
Builder {
under_construction: self.under_construction,
set: (self.set.0, self.set.1, PhantomData::<Set>),
}
}
}
};
}
constructor!(RightHandedXyzLike, [x, y, z]);
constructor!(NedLike, [ned_north, ned_east, ned_down]);
constructor!(FrdLike, [frd_front, frd_right, frd_down]);
constructor!(EnuLike, [enu_east, enu_north, enu_up]);
#[cfg(test)]
mod tests {
use super::Length;
use crate::Point3;
use crate::bearing;
use crate::coordinate_systems::{Ecef, Enu, Frd, Ned};
use crate::coordinates::Coordinate;
use approx::assert_relative_eq;
use rstest::rstest;
fn m(meters: f64) -> Length {
Length::new::<uom::si::length::meter>(meters)
}
#[rstest]
#[case(Point3::new(500., 0., 0.), m(500.))]
#[case(Point3::new(0., 300., 0.), m(300.))]
#[case(Point3::new(0., 0., 200.), m(200.))]
#[case(Point3::new(-500., 0., 0.), m(500.))]
#[case(Point3::new(0., -300., 0.), m(300.))]
#[case(Point3::new(0., 0., -200.), m(200.))]
fn frd_coordinate_distance_to_origin(#[case] point_in_a: Point3, #[case] expected: Length) {
let coordinate = Coordinate::<Frd>::from_nalgebra_point(point_in_a);
assert_eq!(coordinate.distance_from_origin(), expected);
}
#[test]
fn neg_works() {
let frd = coordinate!(f = m(10.), r = m(-5.), d = m(3.5); in Frd);
let ned = coordinate!(n = m(10.), e = m(-5.), d = m(3.5); in Ned);
let ecef = coordinate!(x = m(10.), y = m(-5.), z = m(3.5); in Ecef);
assert_relative_eq!(-frd, coordinate!(f = m(-10.), r = m(5.), d = m(-3.5)));
assert_relative_eq!(-ned, coordinate!(n = m(-10.), e = m(5.), d = m(-3.5)));
assert_relative_eq!(-ecef, coordinate!(x = m(-10.), y = m(5.), z = m(-3.5)));
}
#[test]
fn enu_bearing_from_origin() {
let up = coordinate!(e = m(0.), n = m(0.), u = m(10.0); in Enu);
let down = coordinate!(e = m(0.), n = m(0.), u = m(-10.0); in Enu);
assert_relative_eq!(
up.bearing_from_origin().expect("up is not at origin"),
bearing!(azimuth = deg(0.), elevation = deg(90.); in Enu)
);
assert_relative_eq!(
down.bearing_from_origin().expect("down is not at origin"),
bearing!(azimuth = deg(0.), elevation = deg(-90.); in Enu)
);
}
}