use super::vector::Displacement;
use super::xyz::XYZ;
use crate::centers::ReferenceCenter;
use crate::frames::ReferenceFrame;
use qtty::length::LengthUnit;
use qtty::Quantity;
use std::marker::PhantomData;
use std::ops::{Add, Sub};
#[cfg(feature = "serde")]
#[path = "position_serde.rs"]
mod position_serde;
#[derive(Debug, Clone)]
pub struct CenterParamsMismatchError {
pub operation: &'static str,
}
impl std::fmt::Display for CenterParamsMismatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"center parameter mismatch in `{}`: \
positions reference different parameterized centers \
(e.g., different observer sites)",
self.operation
)
}
}
impl std::error::Error for CenterParamsMismatchError {}
#[derive(Debug, Clone, Copy)]
pub struct Position<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
xyz: XYZ<Quantity<U>>,
center_params: C::Params,
_frame: PhantomData<F>,
}
impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
#[inline]
pub fn new_with_params<T: Into<Quantity<U>>>(
center_params: C::Params,
x: T,
y: T,
z: T,
) -> Self {
Self {
xyz: XYZ::new(x.into(), y.into(), z.into()),
center_params,
_frame: PhantomData,
}
}
#[inline]
pub(crate) fn from_xyz_with_params(center_params: C::Params, xyz: XYZ<Quantity<U>>) -> Self {
Self {
xyz,
center_params,
_frame: PhantomData,
}
}
#[inline]
pub fn from_array(center_params: C::Params, arr: [Quantity<U>; 3]) -> Self {
Self {
xyz: XYZ::from_array(arr),
center_params,
_frame: PhantomData,
}
}
#[inline]
pub const fn new_const(
center_params: C::Params,
x: Quantity<U>,
y: Quantity<U>,
z: Quantity<U>,
) -> Self {
Self {
xyz: XYZ::new(x, y, z),
center_params,
_frame: PhantomData,
}
}
#[inline]
pub fn center_params(&self) -> &C::Params {
&self.center_params
}
}
impl<C, F, U> Position<C, F, U>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
U: LengthUnit,
{
#[inline]
pub fn new<T: Into<Quantity<U>>>(x: T, y: T, z: T) -> Self {
Self::new_with_params((), x, y, z)
}
#[inline]
pub fn from_array_origin(arr: [Quantity<U>; 3]) -> Self {
Self::from_array((), arr)
}
pub const CENTER: Self = Self::new_const(
(),
Quantity::<U>::new(0.0),
Quantity::<U>::new(0.0),
Quantity::<U>::new(0.0),
);
}
impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
#[inline]
pub fn x(&self) -> Quantity<U> {
self.xyz.x()
}
#[inline]
pub fn y(&self) -> Quantity<U> {
self.xyz.y()
}
#[inline]
pub fn z(&self) -> Quantity<U> {
self.xyz.z()
}
#[inline]
pub fn as_array(&self) -> &[Quantity<U>; 3] {
self.xyz.as_array()
}
#[inline]
pub fn to_unit<U2: LengthUnit>(&self) -> Position<C, F, U2>
where
C::Params: Clone,
{
Position::<C, F, U2>::new_with_params(
self.center_params.clone(),
self.x().to::<U2>(),
self.y().to::<U2>(),
self.z().to::<U2>(),
)
}
#[inline]
pub fn reinterpret_frame<F2: ReferenceFrame>(self) -> Position<C, F2, U>
where
C::Params: Clone,
{
Position::new_with_params(self.center_params.clone(), self.x(), self.y(), self.z())
}
}
impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
#[inline]
pub fn distance(&self) -> Quantity<U> {
self.xyz.magnitude()
}
#[inline]
pub fn distance_to(&self, other: &Self) -> Quantity<U>
where
C: ReferenceCenter<Params = ()>,
{
(self.xyz - other.xyz).magnitude()
}
#[inline]
pub fn try_distance_to(&self, other: &Self) -> Result<Quantity<U>, CenterParamsMismatchError>
where
C::Params: PartialEq,
{
if self.center_params != other.center_params {
return Err(CenterParamsMismatchError {
operation: "distance_to",
});
}
Ok((self.xyz - other.xyz).magnitude())
}
#[inline]
pub fn direction(&self) -> Option<super::Direction<F>> {
self.xyz
.to_raw()
.try_normalize()
.map(super::Direction::from_xyz_unchecked)
}
#[inline]
pub fn direction_unchecked(&self) -> super::Direction<F> {
super::Direction::from_xyz_unchecked(self.xyz.to_raw().normalize_unchecked())
}
#[must_use]
#[inline]
pub fn to_spherical(&self) -> crate::spherical::Position<C, F, U> {
crate::spherical::Position::from_cartesian(self)
}
#[must_use]
#[inline]
pub fn from_spherical(sph: &crate::spherical::Position<C, F, U>) -> Self {
sph.to_cartesian()
}
}
impl<C, F, U> Sub for Position<C, F, U>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
U: LengthUnit,
{
type Output = Displacement<F, U>;
#[inline]
fn sub(self, other: Self) -> Self::Output {
Displacement::from_xyz(self.xyz - other.xyz)
}
}
impl<C, F, U> Sub<&Position<C, F, U>> for &Position<C, F, U>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
U: LengthUnit,
{
type Output = Displacement<F, U>;
#[inline]
fn sub(self, other: &Position<C, F, U>) -> Self::Output {
Displacement::from_xyz(self.xyz - other.xyz)
}
}
forward_ref_binop_lhs! {
impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
where (
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
U: LengthUnit,
)
}
forward_ref_binop_rhs! {
impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
where (
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
U: LengthUnit,
)
}
impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
#[inline]
pub fn checked_sub(&self, other: &Self) -> Result<Displacement<F, U>, CenterParamsMismatchError>
where
C::Params: PartialEq,
{
if self.center_params != other.center_params {
return Err(CenterParamsMismatchError {
operation: "sub (Position - Position)",
});
}
Ok(Displacement::from_xyz(self.xyz - other.xyz))
}
}
impl<C, F, U> Add<Displacement<F, U>> for Position<C, F, U>
where
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
{
type Output = Self;
#[inline]
fn add(self, displacement: Displacement<F, U>) -> Self::Output {
Self::from_xyz_with_params(
self.center_params.clone(),
self.xyz + XYZ::from_array(*displacement.as_array()),
)
}
}
impl<C, F, U> Sub<Displacement<F, U>> for Position<C, F, U>
where
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
{
type Output = Self;
#[inline]
fn sub(self, displacement: Displacement<F, U>) -> Self::Output {
Self::from_xyz_with_params(
self.center_params.clone(),
self.xyz - XYZ::from_array(*displacement.as_array()),
)
}
}
forward_ref_binop! {
impl[C, F, U] Add, add for Position<C, F, U>, Displacement<F, U>
where (
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
C::Params: Copy,
)
}
forward_ref_binop! {
impl[C, F, U] Sub, sub for Position<C, F, U>, Displacement<F, U>
where (
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
C::Params: Copy,
)
}
impl_quantity_fmt_triplet! {
impl[C, F, U] for Position<C, F, U>
where {
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
},
fmt_each: { Quantity<U>, },
|this, f, FmtOne| {
write!(
f,
"Center: {}, Frame: {}, X: ",
C::center_name(),
F::frame_name()
)?;
FmtOne::fmt(&this.x(), f)?;
write!(f, ", Y: ")?;
FmtOne::fmt(&this.y(), f)?;
write!(f, ", Z: ")?;
FmtOne::fmt(&this.z(), f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
#[allow(unused_imports)]
use qtty::angular::{Degrees, Radians};
#[allow(unused_imports)]
use qtty::length::{Kilometers, Meters};
use qtty::units::{Kilometer, Meter};
use qtty::M;
#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct TestFrame;
#[derive(Debug, Copy, Clone, ReferenceCenter)]
struct TestCenter;
#[derive(Clone, Debug, Default, PartialEq)]
struct TestParams {
id: i32,
}
#[derive(Debug, Copy, Clone, ReferenceCenter)]
#[center(params = TestParams)]
struct ParamCenter;
type TestPos = Position<TestCenter, TestFrame, Meter>;
type TestDisp = Displacement<TestFrame, Meter>;
#[test]
fn test_position_minus_position_gives_vector() {
let a = TestPos::new(1.0, 2.0, 3.0);
let b = TestPos::new(4.0, 5.0, 6.0);
let displacement: TestDisp = b - a;
assert!((displacement.x().value() - 3.0).abs() < 1e-12);
assert!((displacement.y().value() - 3.0).abs() < 1e-12);
assert!((displacement.z().value() - 3.0).abs() < 1e-12);
}
#[test]
fn test_position_plus_vector_gives_position() {
let pos = TestPos::new(1.0, 2.0, 3.0);
let vec = TestDisp::new(1.0, 1.0, 1.0);
let result: TestPos = pos + vec;
assert!((result.x().value() - 2.0).abs() < 1e-12);
assert!((result.y().value() - 3.0).abs() < 1e-12);
assert!((result.z().value() - 4.0).abs() < 1e-12);
}
#[test]
fn test_position_roundtrip() {
let a = TestPos::new(1.0, 2.0, 3.0);
let b = TestPos::new(4.0, 5.0, 6.0);
let displacement = b - a;
let result = a + displacement;
assert!((result.x().value() - b.x().value()).abs() < 1e-12);
assert!((result.y().value() - b.y().value()).abs() < 1e-12);
assert!((result.z().value() - b.z().value()).abs() < 1e-12);
}
#[test]
fn test_position_distance() {
let pos = TestPos::new(3.0, 4.0, 0.0);
assert!((pos.distance().value() - 5.0).abs() < 1e-12);
}
#[test]
fn test_position_direction() {
let pos = TestPos::new(3.0, 4.0, 0.0);
let dir = pos.direction().expect("non-zero position");
let norm = (dir.x() * dir.x() + dir.y() * dir.y() + dir.z() * dir.z()).sqrt();
assert!((norm - 1.0).abs() < 1e-12);
assert!((dir.x() - 0.6).abs() < 1e-12);
assert!((dir.y() - 0.8).abs() < 1e-12);
}
#[test]
fn test_position_with_params_and_from_array() {
let params = TestParams { id: 42 };
let pos = Position::<ParamCenter, TestFrame, Meter>::new_with_params(
params.clone(),
1.0,
2.0,
3.0,
);
assert_eq!(pos.center_params(), ¶ms);
let arr = [1.0 * M, 2.0 * M, 3.0 * M];
let pos_from_arr =
Position::<ParamCenter, TestFrame, Meter>::from_array(params.clone(), arr);
assert_eq!(pos_from_arr.center_params(), ¶ms);
assert!((pos_from_arr.z().value() - 3.0).abs() < 1e-12);
}
#[test]
fn test_position_from_array_origin_and_center() {
let arr = [1.0 * M, 2.0 * M, 3.0 * M];
let pos = Position::<TestCenter, TestFrame, Meter>::from_array_origin(arr);
assert!((pos.x().value() - 1.0).abs() < 1e-12);
let origin = Position::<TestCenter, TestFrame, Meter>::CENTER;
assert!(origin.distance().value().abs() < 1e-12);
}
#[test]
fn test_position_distance_to_and_sub_methods() {
let a = TestPos::new(0.0, 0.0, 0.0);
let b = TestPos::new(0.0, 3.0, 4.0);
let dist = a.distance_to(&b);
assert!((dist.value() - 5.0).abs() < 1e-12);
let disp: TestDisp = b - a;
assert!((disp.y().value() - 3.0).abs() < 1e-12);
}
#[test]
fn test_position_direction_unchecked_and_sub_displacement() {
let pos = TestPos::new(0.0, 3.0, 4.0);
let dir = pos.direction_unchecked();
assert!((dir.y() - 0.6).abs() < 1e-12);
assert!((dir.z() - 0.8).abs() < 1e-12);
let disp = TestDisp::new(1.0, 1.0, 1.0);
let moved = pos - disp;
assert!((moved.y().value() - 2.0).abs() < 1e-12);
}
#[test]
fn test_position_spherical_roundtrip() {
let pos = TestPos::new(1.0, 1.0, 1.0);
let sph = pos.to_spherical();
let back = TestPos::from_spherical(&sph);
assert!((back.x().value() - pos.x().value()).abs() < 1e-12);
assert!((back.y().value() - pos.y().value()).abs() < 1e-12);
assert!((back.z().value() - pos.z().value()).abs() < 1e-12);
}
#[test]
fn test_position_const_vec3_and_display() {
let pos =
Position::<TestCenter, TestFrame, Meter>::new_const((), 1.0 * M, 2.0 * M, 3.0 * M);
let vec3 = pos.as_array();
assert!((vec3[0].value() - 1.0).abs() < 1e-12);
assert!((vec3[1].value() - 2.0).abs() < 1e-12);
assert!((vec3[2].value() - 3.0).abs() < 1e-12);
let text = pos.to_string();
assert!(text.contains("Center: TestCenter"));
assert!(text.contains("Frame: TestFrame"));
}
#[test]
fn test_position_display_respects_format_specifiers() {
let pos = TestPos::new(1.234_567, -2.0, 3.5);
let text_prec = format!("{:.2}", pos);
let expected_x_prec = format!("{:.2}", pos.x());
assert!(text_prec.contains(&format!("X: {expected_x_prec}")));
let text_exp = format!("{:.3e}", pos);
let expected_z_exp = format!("{:.3e}", pos.z());
assert!(text_exp.contains(&format!("Z: {expected_z_exp}")));
}
#[test]
fn test_position_sub_ref_ref() {
let a = TestPos::new(1.0, 2.0, 3.0);
let b = TestPos::new(4.0, 6.0, 9.0);
let displacement: TestDisp = b - a;
assert!((displacement.x().value() - 3.0).abs() < 1e-12);
assert!((displacement.y().value() - 4.0).abs() < 1e-12);
assert!((displacement.z().value() - 6.0).abs() < 1e-12);
}
type ParamPos = Position<ParamCenter, TestFrame, Meter>;
#[test]
fn test_distance_to_same_params_succeeds() {
let params = TestParams { id: 1 };
let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
let d = a.try_distance_to(&b).unwrap();
assert!((d.value() - 5.0).abs() < 1e-12);
}
#[test]
fn test_distance_to_mismatched_params_err() {
let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
assert!(a.try_distance_to(&b).is_err());
}
#[test]
fn test_try_distance_to_same_params_ok() {
let params = TestParams { id: 1 };
let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
let result = a.try_distance_to(&b);
assert!(result.is_ok());
assert!((result.unwrap().value() - 5.0).abs() < 1e-12);
}
#[test]
fn test_try_distance_to_mismatched_params_err() {
let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
let result = a.try_distance_to(&b);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("center parameter mismatch"));
}
#[test]
fn test_checked_sub_same_params_ok() {
let params = TestParams { id: 1 };
let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
let result = b.checked_sub(&a);
assert!(result.is_ok());
let disp = result.unwrap();
assert!((disp.x().value() - 3.0).abs() < 1e-12);
assert!((disp.y().value() - 4.0).abs() < 1e-12);
}
#[test]
fn test_checked_sub_mismatched_params_err() {
let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
let result = b.checked_sub(&a);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("center parameter mismatch"));
}
#[test]
fn test_unit_params_operations_always_succeed() {
let a = TestPos::new(0.0, 0.0, 0.0);
let b = TestPos::new(3.0, 4.0, 0.0);
assert!((a.distance_to(&b).value() - 5.0).abs() < 1e-12);
assert!(a.try_distance_to(&b).is_ok());
assert!(b.checked_sub(&a).is_ok());
}
#[test]
fn test_center_params_mismatch_error_display() {
let err = CenterParamsMismatchError {
operation: "test_op",
};
let msg = err.to_string();
assert!(msg.contains("test_op"));
assert!(msg.contains("center parameter mismatch"));
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_position_to_unit_roundtrip() {
let p_m = Position::<TestCenter, TestFrame, Meter>::new(1.0, -0.5, 2.25);
let p_km: Position<TestCenter, TestFrame, Kilometer> = p_m.to_unit();
let back: Position<TestCenter, TestFrame, Meter> = p_km.to_unit();
assert!((back.x().value() - p_m.x().value()).abs() < 1e-12);
assert!((back.y().value() - p_m.y().value()).abs() < 1e-12);
assert!((back.z().value() - p_m.z().value()).abs() < 1e-12);
}
#[test]
fn test_position_to_unit_preserves_center_params() {
let p_m = ParamPos::new_with_params(TestParams { id: 7 }, 1.0, 2.0, 3.0);
let p_km: Position<ParamCenter, TestFrame, Kilometer> = p_m.to_unit();
assert_eq!(p_km.center_params().id, 7);
assert!((p_km.x().value() - 0.001).abs() < 1e-12);
assert!((p_km.y().value() - 0.002).abs() < 1e-12);
assert!((p_km.z().value() - 0.003).abs() < 1e-12);
}
}