use crate::coordinates::Coordinate;
use crate::math::{RigidBodyTransform, Rotation};
use crate::systems::EquivalentTo;
use crate::{Point3, Vector};
use core::marker::PhantomData;
use core::ops::Mul;
use uom::ConstZero;
use uom::si::f64::{Angle, Length};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(doc)]
use crate::{
Bearing, CoordinateSystem,
systems::{Ecef, FrdLike, NedLike},
};
#[derive(Clone, Copy, Debug)]
pub(crate) struct ObjectCoordinateSystem;
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(bound = ""))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Orientation<In> {
pub(crate) inner: Rotation<In, ObjectCoordinateSystem>,
}
impl<In> Clone for Orientation<In> {
fn clone(&self) -> Self {
*self
}
}
impl<In> Copy for Orientation<In> {}
impl<In> PartialEq<Self> for Orientation<In> {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(&other.inner)
}
}
impl<In> Orientation<In> {
#[doc(alias = "from_nautical_angles")]
#[doc(alias = "from_cardan_angles")]
#[doc(alias = "from_ypr")]
#[deprecated = "Prefer `tait_bryan_builder` to avoid argument-order confusion"]
pub fn from_tait_bryan_angles(
yaw: impl Into<Angle>,
pitch: impl Into<Angle>,
roll: impl Into<Angle>,
) -> Self {
Self {
#[allow(deprecated)]
inner: unsafe { Rotation::from_tait_bryan_angles(yaw, pitch, roll) },
}
}
#[doc(alias = "from_versor")]
#[must_use]
pub unsafe fn from_quaternion(w: f64, i: f64, j: f64, k: f64) -> Self {
Self {
inner: unsafe { Rotation::from_quaternion(w, i, j, k) },
}
}
#[doc(alias = "to_versor")]
#[must_use]
pub fn to_quaternion(&self) -> (f64, f64, f64, f64) {
self.inner.to_quaternion()
}
#[must_use]
pub fn aligned() -> Self {
Self::tait_bryan_builder()
.yaw(Angle::ZERO)
.pitch(Angle::ZERO)
.roll(Angle::ZERO)
.build()
}
pub fn tait_bryan_builder() -> crate::math::tait_bryan_builder::TaitBryanBuilder<
crate::math::tait_bryan_builder::NeedsYaw,
Orientation<In>,
> {
crate::math::tait_bryan_builder::TaitBryanBuilder::new()
}
}
impl<In> Default for Orientation<In> {
fn default() -> Self {
Self::aligned()
}
}
impl<In> Orientation<In> {
#[doc(alias = "as_transform_to")]
#[doc(alias = "defines_pose_of")]
#[must_use]
pub unsafe fn map_as_zero_in<To>(self) -> Rotation<In, To> {
Rotation {
inner: self.inner.inner,
from: self.inner.from,
to: PhantomData::<To>,
}
}
#[must_use]
pub fn cast<NewIn>(self) -> Orientation<NewIn>
where
In: EquivalentTo<NewIn>,
{
Orientation {
inner: self.inner.cast_type_of_from::<NewIn>(),
}
}
#[must_use]
pub fn to_tait_bryan_angles(&self) -> (Angle, Angle, Angle) {
self.inner.to_tait_bryan_angles()
}
#[must_use]
pub fn nlerp(&self, rhs: &Self, t: f64) -> Self {
Self {
inner: self.inner.nlerp(&rhs.inner, t),
}
}
}
impl<From, To> Mul<Rotation<From, To>> for Orientation<From> {
type Output = Orientation<To>;
fn mul(self, rhs: Rotation<From, To>) -> Self::Output {
if false {
let _ = rhs.inverse() * self;
}
rhs.inverse() * self
}
}
impl<From, To> Mul<Orientation<To>> for Rotation<From, To> {
type Output = Orientation<From>;
fn mul(self, rhs: Orientation<To>) -> Self::Output {
Orientation {
inner: self * rhs.inner,
}
}
}
impl<From, To> Mul<RigidBodyTransform<From, To>> for Orientation<From> {
type Output = Pose<To>;
fn mul(self, rhs: RigidBodyTransform<From, To>) -> Self::Output {
if false {
let _ = rhs.inverse() * self;
}
rhs.inverse() * self
}
}
impl<From, To> Mul<Orientation<To>> for RigidBodyTransform<From, To> {
type Output = Pose<From>;
fn mul(self, rhs: Orientation<To>) -> Self::Output {
Pose {
inner: self * rhs.inner,
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(bound = ""))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Pose<In> {
pub(crate) inner: RigidBodyTransform<In, ObjectCoordinateSystem>,
}
impl<In> Clone for Pose<In> {
fn clone(&self) -> Self {
*self
}
}
impl<In> Copy for Pose<In> {}
impl<In> PartialEq<Self> for Pose<In> {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(&other.inner)
}
}
impl<In> Pose<In> {
#[must_use]
pub fn new(position: Coordinate<In>, orientation: Orientation<In>) -> Self {
Self {
inner: unsafe {
RigidBodyTransform::new(Vector::from(position), orientation.map_as_zero_in())
},
}
}
#[doc(alias = "as_transform_to")]
#[doc(alias = "defines_pose_of")]
#[doc(alias = "is_position_and_facing_direction_of")]
#[must_use]
pub unsafe fn map_as_zero_in<To>(self) -> RigidBodyTransform<In, To> {
RigidBodyTransform {
inner: self.inner.inner,
from: self.inner.from,
to: PhantomData::<To>,
}
}
#[must_use]
pub fn cast<NewIn>(self) -> Pose<NewIn>
where
In: EquivalentTo<NewIn>,
{
Pose {
inner: self.inner.cast_type_of_from::<NewIn>(),
}
}
#[must_use]
pub fn lerp(&self, rhs: &Self, t: f64) -> Self {
Self {
inner: self.inner.lerp(&rhs.inner, t),
}
}
}
impl<In> Default for Pose<In> {
fn default() -> Self {
Self::new(Coordinate::default(), Orientation::default())
}
}
impl<In> Pose<In> {
#[must_use]
pub fn position(&self) -> Coordinate<In> {
Coordinate::from_nalgebra_point(Point3::from(self.inner.translation().inner))
}
#[must_use]
pub fn orientation(&self) -> Orientation<In> {
Orientation {
inner: self.inner.rotation(),
}
}
#[must_use]
pub fn distance_from_origin(&self) -> Length {
self.inner.translation().magnitude()
}
}
impl<From, To> Mul<RigidBodyTransform<From, To>> for Pose<From> {
type Output = Pose<To>;
fn mul(self, rhs: RigidBodyTransform<From, To>) -> Self::Output {
if false {
let _ = rhs.inverse() * self;
}
Pose {
inner: RigidBodyTransform {
inner: rhs.inner.inv_mul(&self.inner.inner),
from: rhs.to,
to: self.inner.to,
},
}
}
}
impl<From, To> Mul<Pose<To>> for RigidBodyTransform<From, To> {
type Output = Pose<From>;
fn mul(self, rhs: Pose<To>) -> Self::Output {
Pose {
inner: self * rhs.inner,
}
}
}
#[cfg(test)]
mod tests {
use crate::coordinate_systems::{Ecef, Ned};
use crate::coordinates::Coordinate;
use crate::directions::{Bearing, Components as BearingComponents};
use crate::engineering::{Orientation, Pose};
use crate::float_math::FloatMath;
use crate::geodetic::{Components as Wgs84Components, Wgs84};
use crate::math::{RigidBodyTransform, Rotation};
use crate::util::BoundedAngle;
use crate::vectors::Vector;
use crate::{Point3, coordinate};
use approx::assert_relative_eq;
use approx::{AbsDiffEq, assert_abs_diff_eq};
use rstest::rstest;
use std::println;
use uom::si::f64::{Angle, Length};
use uom::si::{
angle::{degree, radian},
length::meter,
};
fn m(meters: f64) -> Length {
Length::new::<meter>(meters)
}
fn r(radians: f64) -> Angle {
Angle::new::<radian>(radians)
}
fn d(degrees: f64) -> Angle {
Angle::new::<degree>(degrees)
}
system!(struct PlaneFrd using FRD);
system!(struct PlaneNed using NED);
system!(struct PlaneBNed using NED);
system!(struct SensorFrd using FRD);
#[test]
fn usecase_1_where_is_the_object_in_world() {
let observation = Coordinate::<PlaneFrd>::from_bearing(
Bearing::build(BearingComponents {
azimuth: d(20.),
elevation: d(10.),
})
.expect("elevation is in-range"),
m(400.),
);
let wgs84 = Wgs84::build(Wgs84Components {
latitude: d(12.),
longitude: d(30.),
altitude: m(1000.),
})
.expect("latitude is in-range");
let orientation_in_ned = Orientation::<PlaneNed>::tait_bryan_builder()
.yaw(d(0.))
.pitch(d(45.))
.roll(d(0.))
.build();
let ecef_to_plane_ned = unsafe { RigidBodyTransform::ecef_to_ned_at(&wgs84) };
let plane_ned_to_plane_frd = unsafe { orientation_in_ned.map_as_zero_in::<PlaneFrd>() };
let world_to_plane_frd = ecef_to_plane_ned.and_then(plane_ned_to_plane_frd);
let observation_in_world_eng1 = world_to_plane_frd.inverse_transform(observation);
println!("{:?}", observation_in_world_eng1.to_wgs84());
let pose_in_world = ecef_to_plane_ned.inverse_transform(orientation_in_ned);
let transformation_world_to_frd_eng2 =
unsafe { pose_in_world.map_as_zero_in::<PlaneFrd>() };
let observation_in_world_eng2 =
transformation_world_to_frd_eng2.inverse_transform(observation);
assert_relative_eq!(observation_in_world_eng1, observation_in_world_eng2);
let ecef_to_ned = unsafe { RigidBodyTransform::<Ecef, PlaneNed>::ecef_to_ned_at(&wgs84) };
let ned_to_frd = unsafe {
Rotation::tait_bryan_builder()
.yaw(d(0.))
.pitch(d(45.))
.roll(d(0.))
.build()
};
let ecef_to_frd = ecef_to_ned * ned_to_frd;
let observation_in_world_math = ecef_to_frd.inverse_transform(observation);
assert_relative_eq!(observation_in_world_math, observation_in_world_eng1);
}
#[test]
fn usecase_2_4() {
let position_plane_a = Wgs84::build(Wgs84Components {
latitude: d(12.),
longitude: d(30.2),
altitude: m(1000.),
})
.expect("latitude is in-range");
let position_plane_b = Wgs84::build(Wgs84Components {
latitude: d(12.),
longitude: d(30.),
altitude: m(1000.),
})
.expect("latitude is in-range");
let orientation_plane_a_in_ned = Orientation::<PlaneNed>::tait_bryan_builder()
.yaw(d(0.))
.pitch(d(45.))
.roll(d(0.))
.build();
let orientation_plane_b_in_ned = Orientation::tait_bryan_builder()
.yaw(d(20.))
.pitch(d(12.))
.roll(d(0.))
.build();
let ecef_to_plane_a_ned = unsafe { RigidBodyTransform::ecef_to_ned_at(&position_plane_a) };
let pose_plane_a_in_world =
ecef_to_plane_a_ned.inverse_transform(orientation_plane_a_in_ned);
let ecef_to_plane_b_ned =
unsafe { RigidBodyTransform::<Ecef, PlaneBNed>::ecef_to_ned_at(&position_plane_b) };
let pose_plane_b_in_world =
ecef_to_plane_b_ned.inverse_transform(orientation_plane_b_in_ned);
let _surface_distance = position_plane_b.haversine_distance_on_surface(&position_plane_a);
let free_space_distance = (Coordinate::from_wgs84(&position_plane_b)
- Coordinate::from_wgs84(&position_plane_a))
.magnitude();
let free_space_distance_from_pose =
(pose_plane_b_in_world.position() - pose_plane_a_in_world.position()).magnitude();
assert_eq!(free_space_distance, free_space_distance_from_pose);
let plane_a_frd_to_ecef =
unsafe { pose_plane_a_in_world.map_as_zero_in::<PlaneFrd>() }.inverse();
let pose_plane_b_for_plane_a = plane_a_frd_to_ecef.inverse_transform(pose_plane_b_in_world);
let direction_to_plane_b_for_plane_a = pose_plane_b_for_plane_a
.position()
.bearing_from_origin()
.expect("azimuth is well-defined");
println!(
"Direction towards plane B as observed by plane A {direction_to_plane_b_for_plane_a}"
);
}
#[rstest]
#[case(
Point3::new(10., 0., 0.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(0.), elevation: d(0.) }).unwrap()
)]
#[case(
Point3::new(0., 10., 0.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(90.), elevation: d(0.) }).unwrap()
)]
#[case(
Point3::new(-10., 0., 0.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(-180.), elevation: d(0.) }).unwrap()
)]
#[case(
Point3::new(0., -10., 0.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(270.), elevation: d(0.) }).unwrap()
)]
#[case(
Point3::new(0., 0., 10.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(0.), elevation: d(-90.) }).unwrap()
)]
#[case(
Point3::new(0., -10., 10.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(270.), elevation: d(-45.) }).unwrap()
)]
#[case(
Point3::new(10., 10., -10.),
(d(0.), d(0.), d(0.)),
Bearing::build(BearingComponents { azimuth: d(45.), elevation: r(FloatMath::asin(10. / FloatMath::sqrt(FloatMath::powi(10_f64, 2) * 3.))) }).unwrap()
)]
fn pose_direction_towards(
#[case] position: Point3,
#[case] ypr: (Angle, Angle, Angle),
#[case] expected: Bearing<Ned>,
) {
let (yaw, pitch, roll) = ypr;
let pose = Pose::<Ned>::new(
Coordinate::from_nalgebra_point(position),
Orientation::tait_bryan_builder()
.yaw(yaw)
.pitch(pitch)
.roll(roll)
.build(),
);
if pitch <= d(90.) {
let (y, p, r) = pose.orientation().to_tait_bryan_angles();
for (a, b) in [(y, yaw), (p, pitch), (r, roll)] {
assert_abs_diff_eq!(&BoundedAngle::new(a), &BoundedAngle::new(b),);
}
}
let direction = pose.position().bearing_from_origin().unwrap();
assert_abs_diff_eq!(direction, expected);
}
#[test]
fn orientation_inverse_works() {
let ned_to_frd = unsafe {
Rotation::<PlaneNed, PlaneFrd>::tait_bryan_builder()
.yaw(d(45.))
.pitch(d(85.))
.roll(d(150.))
.build()
};
let frd_to_ned = ned_to_frd.inverse();
let frd = coordinate!(f = m(42.), r = m(69.), d = m(99.); in PlaneFrd);
assert_relative_eq!(ned_to_frd.inverse_transform(frd), frd_to_ned.transform(frd));
let ned = coordinate!(n = m(42.), e = m(69.), d = m(99.); in PlaneNed);
assert_relative_eq!(ned_to_frd.transform(ned), frd_to_ned.inverse_transform(ned));
}
#[test]
fn pose_multiplication_works() {
let ned_to_frd = unsafe {
RigidBodyTransform::<PlaneNed, PlaneFrd>::new(
Vector::<PlaneNed>::zero(),
Rotation::tait_bryan_builder()
.yaw(d(90.))
.pitch(d(90.))
.roll(d(0.))
.build(),
)
};
let wgs84 = Wgs84::build(Wgs84Components {
latitude: d(52.),
longitude: d(-3.),
altitude: m(1000.),
})
.expect("latitude is in-range");
let ecef_to_ned = unsafe { RigidBodyTransform::<Ecef, PlaneNed>::ecef_to_ned_at(&wgs84) };
let ecef_to_frd = ecef_to_ned * ned_to_frd;
let forward = coordinate!(f = m(1.), r = m(0.), d = m(0.); in PlaneFrd);
let right = coordinate!(f = m(0.), r = m(1.), d = m(0.); in PlaneFrd);
let down = coordinate!(f = m(0.), r = m(0.), d = m(1.); in PlaneFrd);
let forward_in_ned = ned_to_frd.inverse_transform(forward);
let right_in_ned = ned_to_frd.inverse_transform(right);
let down_in_ned = ned_to_frd.inverse_transform(down);
assert_relative_eq!(
forward_in_ned,
-coordinate!(n = m(0.), e = m(0.), d = m(1.))
);
assert_relative_eq!(right_in_ned, -coordinate!(n = m(1.), e = m(0.), d = m(0.)));
assert_relative_eq!(down_in_ned, coordinate!(n = m(0.), e = m(1.), d = m(0.)));
let forward_in_ecef = ecef_to_ned.inverse_transform(forward_in_ned);
let right_in_ecef = ecef_to_ned.inverse_transform(right_in_ned);
let down_in_ecef = ecef_to_ned.inverse_transform(down_in_ned);
let frd_to_ecef = ecef_to_frd.inverse();
assert_relative_eq!(forward_in_ecef, ecef_to_frd.inverse_transform(forward));
assert_relative_eq!(
frd_to_ecef.transform(forward),
ecef_to_frd.inverse_transform(forward),
);
assert_relative_eq!(right_in_ecef, ecef_to_frd.inverse_transform(right));
assert_relative_eq!(
frd_to_ecef.transform(right),
ecef_to_frd.inverse_transform(right),
);
assert_relative_eq!(down_in_ecef, ecef_to_frd.inverse_transform(down));
assert_relative_eq!(
frd_to_ecef.transform(down),
ecef_to_frd.inverse_transform(down),
);
assert_relative_eq!(
Coordinate::<Ecef>::from_wgs84(&wgs84),
ecef_to_frd.inverse_transform(Coordinate::<PlaneFrd>::origin()),
);
let point_a_frd = coordinate!(f = m(20.), r = m(-45.), d = m(10.); in PlaneFrd);
let point_b_frd = coordinate!(f = m(100.), r = m(-200.), d = m(50.); in PlaneFrd);
assert_relative_eq!(
(point_b_frd - point_a_frd).magnitude().get::<meter>(),
(ecef_to_frd.inverse_transform(point_b_frd)
- ecef_to_frd.inverse_transform(point_a_frd))
.magnitude()
.get::<meter>(),
epsilon = Coordinate::<Ecef>::default_epsilon().get::<meter>(),
);
let ned_to_frd_2 = unsafe {
RigidBodyTransform::<PlaneBNed, SensorFrd>::new(
Vector::<PlaneBNed>::zero(),
Rotation::tait_bryan_builder()
.yaw(d(-45.))
.pitch(d(0.))
.roll(d(0.))
.build(),
)
};
let ecef_to_ned_2 = unsafe {
RigidBodyTransform::<Ecef, PlaneBNed>::ecef_to_ned_at(
&Wgs84::build(Wgs84Components {
latitude: d(54.),
longitude: d(-3.67),
altitude: m(0.),
})
.expect("latitude is in-range"),
)
};
let ecef_to_frd_2 = ecef_to_ned_2 * ned_to_frd_2;
let frd_1_to_frd_2 = ecef_to_frd.inverse() * ecef_to_frd_2;
let forward_in_ned_2 = ecef_to_ned_2.transform(forward_in_ecef);
let right_in_ned_2 = ecef_to_ned_2.transform(right_in_ecef);
let down_in_ned_2 = ecef_to_ned_2.transform(down_in_ecef);
let forward_in_frd_2 = ned_to_frd_2.transform(forward_in_ned_2);
let right_in_frd_2 = ned_to_frd_2.transform(right_in_ned_2);
let down_in_frd_2 = ned_to_frd_2.transform(down_in_ned_2);
let forward_chained = frd_1_to_frd_2.transform(forward);
let right_chained = frd_1_to_frd_2.transform(right);
let down_chained = frd_1_to_frd_2.transform(down);
assert_relative_eq!(forward_in_frd_2, forward_chained);
assert_relative_eq!(right_in_frd_2, right_chained);
assert_relative_eq!(down_in_frd_2, down_chained);
}
#[test]
fn orientation_quaternion_identity() {
let identity = unsafe { Orientation::<PlaneNed>::from_quaternion(1.0, 0.0, 0.0, 0.0) };
assert_eq!(identity, Orientation::<PlaneNed>::aligned());
}
#[rstest]
#[case(d(0.), d(0.), d(0.))]
#[case(d(90.), d(0.), d(0.))]
#[case(d(0.), d(45.), d(0.))]
#[case(d(0.), d(0.), d(30.))]
#[case(d(90.), d(45.), d(5.))]
#[case(d(39.), d(45.), d(55.))]
#[case(d(180.), d(0.), d(0.))]
#[case(d(-45.), d(20.), d(10.))]
fn orientation_quaternion_round_trip(
#[case] yaw: Angle,
#[case] pitch: Angle,
#[case] roll: Angle,
) {
let from_angles = Orientation::<PlaneNed>::tait_bryan_builder()
.yaw(yaw)
.pitch(pitch)
.roll(roll)
.build();
let (w, i, j, k) = from_angles.to_quaternion();
let from_quat = unsafe { Orientation::<PlaneNed>::from_quaternion(w, i, j, k) };
let (y1, p1, r1) = from_angles.to_tait_bryan_angles();
let (y2, p2, r2) = from_quat.to_tait_bryan_angles();
for (a, b) in [(y1, y2), (p1, p2), (r1, r2)] {
assert_abs_diff_eq!(&BoundedAngle::new(a), &BoundedAngle::new(b));
}
}
}