use super::vector::Displacement;
use super::xyz::XYZ;
use crate::centers::ReferenceCenter;
use crate::frames::ReferenceFrame;
use qtty::{LengthUnit, 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_vec3(center_params: C::Params, vec3: nalgebra::Vector3<Quantity<U>>) -> Self {
Self {
xyz: XYZ::from_vec3(vec3),
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_vec3_origin(vec3: nalgebra::Vector3<Quantity<U>>) -> Self {
Self::from_vec3((), vec3)
}
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_vec3(&self) -> &nalgebra::Vector3<Quantity<U>> {
self.xyz.as_vec3()
}
#[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::Params: PartialEq,
{
assert!(
self.center_params == other.center_params,
"Cannot compute distance between positions with different center parameters"
);
(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,
F: ReferenceFrame,
U: LengthUnit,
{
type Output = Displacement<F, U>;
#[inline]
fn sub(self, other: Self) -> Self::Output {
assert!(
self.center_params == other.center_params,
"Cannot subtract positions with different center parameters"
);
Displacement::from_xyz(self.xyz - other.xyz)
}
}
impl<C, F, U> Sub<&Position<C, F, U>> for &Position<C, F, U>
where
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
{
type Output = Displacement<F, U>;
#[inline]
fn sub(self, other: &Position<C, F, U>) -> Self::Output {
assert!(
self.center_params == other.center_params,
"Cannot subtract positions with different center parameters"
);
Displacement::from_xyz(self.xyz - other.xyz)
}
}
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_vec3(*displacement.as_vec3()),
)
}
}
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_vec3(*displacement.as_vec3()),
)
}
}
impl<C, F, U> std::fmt::Display for Position<C, F, U>
where
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
Quantity<U>: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Center: {}, Frame: {}, X: ",
C::center_name(),
F::frame_name()
)?;
std::fmt::Display::fmt(&self.x(), f)?;
write!(f, ", Y: ")?;
std::fmt::Display::fmt(&self.y(), f)?;
write!(f, ", Z: ")?;
std::fmt::Display::fmt(&self.z(), f)
}
}
impl<C, F, U> std::fmt::LowerExp for Position<C, F, U>
where
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
Quantity<U>: std::fmt::LowerExp,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Center: {}, Frame: {}, X: ",
C::center_name(),
F::frame_name()
)?;
std::fmt::LowerExp::fmt(&self.x(), f)?;
write!(f, ", Y: ")?;
std::fmt::LowerExp::fmt(&self.y(), f)?;
write!(f, ", Z: ")?;
std::fmt::LowerExp::fmt(&self.z(), f)
}
}
impl<C, F, U> std::fmt::UpperExp for Position<C, F, U>
where
C: ReferenceCenter,
F: ReferenceFrame,
U: LengthUnit,
Quantity<U>: std::fmt::UpperExp,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Center: {}, Frame: {}, X: ",
C::center_name(),
F::frame_name()
)?;
std::fmt::UpperExp::fmt(&self.x(), f)?;
write!(f, ", Y: ")?;
std::fmt::UpperExp::fmt(&self.y(), f)?;
write!(f, ", Z: ")?;
std::fmt::UpperExp::fmt(&self.z(), f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
use qtty::*;
#[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_vec3() {
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 vec3 = nalgebra::Vector3::new(1.0 * M, 2.0 * M, 3.0 * M);
let pos_from_vec =
Position::<ParamCenter, TestFrame, Meter>::from_vec3(params.clone(), vec3);
assert_eq!(pos_from_vec.center_params(), ¶ms);
assert!((pos_from_vec.z().value() - 3.0).abs() < 1e-12);
}
#[test]
fn test_position_from_vec3_origin_and_center() {
let vec3 = nalgebra::Vector3::new(1.0 * M, 2.0 * M, 3.0 * M);
let pos = Position::<TestCenter, TestFrame, Meter>::from_vec3_origin(vec3);
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_vec3();
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);
assert!((a.distance_to(&b).value() - 5.0).abs() < 1e-12);
}
#[test]
#[should_panic(expected = "different center parameters")]
fn test_distance_to_mismatched_params_panics() {
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 _ = a.distance_to(&b);
}
#[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]
#[should_panic(expected = "different center parameters")]
fn test_sub_mismatched_params_panics() {
let a = ParamPos::new_with_params(TestParams { id: 1 }, 1.0, 2.0, 3.0);
let b = ParamPos::new_with_params(TestParams { id: 2 }, 4.0, 5.0, 6.0);
let _ = a - b;
}
#[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_au = Position::<TestCenter, TestFrame, AstronomicalUnit>::new(1.0, -0.5, 2.25);
let p_km: Position<TestCenter, TestFrame, Kilometer> = p_au.to_unit();
let back: Position<TestCenter, TestFrame, AstronomicalUnit> = p_km.to_unit();
assert!((back.x().value() - p_au.x().value()).abs() < 1e-12);
assert!((back.y().value() - p_au.y().value()).abs() < 1e-12);
assert!((back.z().value() - p_au.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);
}
}