1use super::vector::Displacement;
67use super::xyz::XYZ;
68use crate::centers::ReferenceCenter;
69use crate::frames::ReferenceFrame;
70use qtty::length::LengthUnit;
71use qtty::Quantity;
72
73use std::marker::PhantomData;
74use std::ops::{Add, Sub};
75
76#[cfg(feature = "serde")]
78#[path = "position_serde.rs"]
79mod position_serde;
80
81#[derive(Debug, Clone)]
94pub struct CenterParamsMismatchError {
95 pub operation: &'static str,
97}
98
99impl std::fmt::Display for CenterParamsMismatchError {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 write!(
102 f,
103 "center parameter mismatch in `{}`: \
104 positions reference different parameterized centers \
105 (e.g., different observer sites)",
106 self.operation
107 )
108 }
109}
110
111impl std::error::Error for CenterParamsMismatchError {}
112
113#[derive(Debug, Clone, Copy)]
128pub struct Position<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
129 xyz: XYZ<Quantity<U>>,
130 center_params: C::Params,
131 _frame: PhantomData<F>,
132}
133
134impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
139 #[inline]
145 pub fn new_with_params<T: Into<Quantity<U>>>(
146 center_params: C::Params,
147 x: T,
148 y: T,
149 z: T,
150 ) -> Self {
151 Self {
152 xyz: XYZ::new(x.into(), y.into(), z.into()),
153 center_params,
154 _frame: PhantomData,
155 }
156 }
157
158 #[inline]
160 pub(crate) fn from_xyz_with_params(center_params: C::Params, xyz: XYZ<Quantity<U>>) -> Self {
161 Self {
162 xyz,
163 center_params,
164 _frame: PhantomData,
165 }
166 }
167
168 #[inline]
170 pub fn from_array(center_params: C::Params, arr: [Quantity<U>; 3]) -> Self {
171 Self {
172 xyz: XYZ::from_array(arr),
173 center_params,
174 _frame: PhantomData,
175 }
176 }
177
178 #[inline]
180 pub const fn new_const(
181 center_params: C::Params,
182 x: Quantity<U>,
183 y: Quantity<U>,
184 z: Quantity<U>,
185 ) -> Self {
186 Self {
187 xyz: XYZ::new(x, y, z),
188 center_params,
189 _frame: PhantomData,
190 }
191 }
192
193 #[inline]
195 pub fn center_params(&self) -> &C::Params {
196 &self.center_params
197 }
198}
199
200impl<C, F, U> Position<C, F, U>
205where
206 C: ReferenceCenter<Params = ()>,
207 F: ReferenceFrame,
208 U: LengthUnit,
209{
210 #[inline]
237 pub fn new<T: Into<Quantity<U>>>(x: T, y: T, z: T) -> Self {
238 Self::new_with_params((), x, y, z)
239 }
240
241 #[inline]
243 pub fn from_array_origin(arr: [Quantity<U>; 3]) -> Self {
244 Self::from_array((), arr)
245 }
246
247 pub const CENTER: Self = Self::new_const(
249 (),
250 Quantity::<U>::new(0.0),
251 Quantity::<U>::new(0.0),
252 Quantity::<U>::new(0.0),
253 );
254}
255
256impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
261 #[inline]
263 pub fn x(&self) -> Quantity<U> {
264 self.xyz.x()
265 }
266
267 #[inline]
269 pub fn y(&self) -> Quantity<U> {
270 self.xyz.y()
271 }
272
273 #[inline]
275 pub fn z(&self) -> Quantity<U> {
276 self.xyz.z()
277 }
278
279 #[inline]
281 pub fn as_array(&self) -> &[Quantity<U>; 3] {
282 self.xyz.as_array()
283 }
284
285 #[inline]
290 pub fn to_unit<U2: LengthUnit>(&self) -> Position<C, F, U2>
291 where
292 C::Params: Clone,
293 {
294 Position::<C, F, U2>::new_with_params(
295 self.center_params.clone(),
296 self.x().to::<U2>(),
297 self.y().to::<U2>(),
298 self.z().to::<U2>(),
299 )
300 }
301
302 #[inline]
320 pub fn reinterpret_frame<F2: ReferenceFrame>(self) -> Position<C, F2, U>
321 where
322 C::Params: Clone,
323 {
324 Position::new_with_params(self.center_params.clone(), self.x(), self.y(), self.z())
325 }
326}
327
328impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
333 #[inline]
335 pub fn distance(&self) -> Quantity<U> {
336 self.xyz.magnitude()
337 }
338
339 #[inline]
355 pub fn distance_to(&self, other: &Self) -> Quantity<U>
356 where
357 C: ReferenceCenter<Params = ()>,
358 {
359 (self.xyz - other.xyz).magnitude()
360 }
361
362 #[inline]
367 pub fn try_distance_to(&self, other: &Self) -> Result<Quantity<U>, CenterParamsMismatchError>
368 where
369 C::Params: PartialEq,
370 {
371 if self.center_params != other.center_params {
372 return Err(CenterParamsMismatchError {
373 operation: "distance_to",
374 });
375 }
376 Ok((self.xyz - other.xyz).magnitude())
377 }
378
379 #[inline]
386 pub fn direction(&self) -> Option<super::Direction<F>> {
387 self.xyz
388 .to_raw()
389 .try_normalize()
390 .map(super::Direction::from_xyz_unchecked)
391 }
392
393 #[inline]
398 pub fn direction_unchecked(&self) -> super::Direction<F> {
399 super::Direction::from_xyz_unchecked(self.xyz.to_raw().normalize_unchecked())
400 }
401
402 #[must_use]
407 #[inline]
408 pub fn to_spherical(&self) -> crate::spherical::Position<C, F, U> {
409 crate::spherical::Position::from_cartesian(self)
410 }
411
412 #[must_use]
416 #[inline]
417 pub fn from_spherical(sph: &crate::spherical::Position<C, F, U>) -> Self {
418 sph.to_cartesian()
419 }
420}
421
422impl<C, F, U> Sub for Position<C, F, U>
427where
428 C: ReferenceCenter<Params = ()>,
429 F: ReferenceFrame,
430 U: LengthUnit,
431{
432 type Output = Displacement<F, U>;
433
434 #[inline]
442 fn sub(self, other: Self) -> Self::Output {
443 Displacement::from_xyz(self.xyz - other.xyz)
444 }
445}
446
447impl<C, F, U> Sub<&Position<C, F, U>> for &Position<C, F, U>
448where
449 C: ReferenceCenter<Params = ()>,
450 F: ReferenceFrame,
451 U: LengthUnit,
452{
453 type Output = Displacement<F, U>;
454
455 #[inline]
460 fn sub(self, other: &Position<C, F, U>) -> Self::Output {
461 Displacement::from_xyz(self.xyz - other.xyz)
462 }
463}
464
465forward_ref_binop_lhs! {
470 impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
471 where (
472 C: ReferenceCenter<Params = ()>,
473 F: ReferenceFrame,
474 U: LengthUnit,
475 )
476}
477forward_ref_binop_rhs! {
478 impl[C, F, U] Sub, sub for Position<C, F, U>, Position<C, F, U>
479 where (
480 C: ReferenceCenter<Params = ()>,
481 F: ReferenceFrame,
482 U: LengthUnit,
483 )
484}
485
486impl<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> Position<C, F, U> {
487 #[inline]
493 pub fn checked_sub(&self, other: &Self) -> Result<Displacement<F, U>, CenterParamsMismatchError>
494 where
495 C::Params: PartialEq,
496 {
497 if self.center_params != other.center_params {
498 return Err(CenterParamsMismatchError {
499 operation: "sub (Position - Position)",
500 });
501 }
502 Ok(Displacement::from_xyz(self.xyz - other.xyz))
503 }
504}
505
506impl<C, F, U> Add<Displacement<F, U>> for Position<C, F, U>
511where
512 C: ReferenceCenter,
513 F: ReferenceFrame,
514 U: LengthUnit,
515{
516 type Output = Self;
517
518 #[inline]
520 fn add(self, displacement: Displacement<F, U>) -> Self::Output {
521 Self::from_xyz_with_params(
522 self.center_params.clone(),
523 self.xyz + XYZ::from_array(*displacement.as_array()),
524 )
525 }
526}
527
528impl<C, F, U> Sub<Displacement<F, U>> for Position<C, F, U>
529where
530 C: ReferenceCenter,
531 F: ReferenceFrame,
532 U: LengthUnit,
533{
534 type Output = Self;
535
536 #[inline]
538 fn sub(self, displacement: Displacement<F, U>) -> Self::Output {
539 Self::from_xyz_with_params(
540 self.center_params.clone(),
541 self.xyz - XYZ::from_array(*displacement.as_array()),
542 )
543 }
544}
545
546forward_ref_binop! {
547 impl[C, F, U] Add, add for Position<C, F, U>, Displacement<F, U>
548 where (
549 C: ReferenceCenter,
550 F: ReferenceFrame,
551 U: LengthUnit,
552 C::Params: Copy,
553 )
554}
555
556forward_ref_binop! {
557 impl[C, F, U] Sub, sub for Position<C, F, U>, Displacement<F, U>
558 where (
559 C: ReferenceCenter,
560 F: ReferenceFrame,
561 U: LengthUnit,
562 C::Params: Copy,
563 )
564}
565
566impl<C, F, U> PartialEq for Position<C, F, U>
571where
572 C: ReferenceCenter,
573 C::Params: PartialEq,
574 F: ReferenceFrame,
575 U: LengthUnit,
576{
577 fn eq(&self, other: &Self) -> bool {
578 self.xyz == other.xyz && self.center_params == other.center_params
579 }
580}
581
582impl_quantity_fmt_triplet! {
587 impl[C, F, U] for Position<C, F, U>
588 where {
589 C: ReferenceCenter,
590 F: ReferenceFrame,
591 U: LengthUnit,
592 },
593 fmt_each: { Quantity<U>, },
594 |this, f, FmtOne| {
595 write!(
596 f,
597 "Center: {}, Frame: {}, X: ",
598 C::center_name(),
599 F::frame_name()
600 )?;
601 FmtOne::fmt(&this.x(), f)?;
602 write!(f, ", Y: ")?;
603 FmtOne::fmt(&this.y(), f)?;
604 write!(f, ", Z: ")?;
605 FmtOne::fmt(&this.z(), f)
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612 use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
614
615 #[allow(unused_imports)]
616 use qtty::angular::{Degrees, Radians};
617 #[allow(unused_imports)]
618 use qtty::length::{Kilometers, Meters};
619 use qtty::units::{Kilometer, Meter};
620 use qtty::M;
621 #[derive(Debug, Copy, Clone, ReferenceFrame)]
623 struct TestFrame;
624 #[derive(Debug, Copy, Clone, ReferenceCenter)]
625 struct TestCenter;
626
627 #[derive(Clone, Debug, Default, PartialEq)]
628 struct TestParams {
629 id: i32,
630 }
631
632 #[derive(Debug, Copy, Clone, ReferenceCenter)]
633 #[center(params = TestParams)]
634 struct ParamCenter;
635
636 type TestPos = Position<TestCenter, TestFrame, Meter>;
637 type TestDisp = Displacement<TestFrame, Meter>;
638
639 #[test]
640 fn test_position_minus_position_gives_vector() {
641 let a = TestPos::new(1.0, 2.0, 3.0);
642 let b = TestPos::new(4.0, 5.0, 6.0);
643
644 let displacement: TestDisp = b - a;
645 assert!((displacement.x().value() - 3.0).abs() < 1e-12);
646 assert!((displacement.y().value() - 3.0).abs() < 1e-12);
647 assert!((displacement.z().value() - 3.0).abs() < 1e-12);
648 }
649
650 #[test]
651 fn test_position_plus_vector_gives_position() {
652 let pos = TestPos::new(1.0, 2.0, 3.0);
653 let vec = TestDisp::new(1.0, 1.0, 1.0);
654
655 let result: TestPos = pos + vec;
656 assert!((result.x().value() - 2.0).abs() < 1e-12);
657 assert!((result.y().value() - 3.0).abs() < 1e-12);
658 assert!((result.z().value() - 4.0).abs() < 1e-12);
659 }
660
661 #[test]
662 fn test_position_roundtrip() {
663 let a = TestPos::new(1.0, 2.0, 3.0);
664 let b = TestPos::new(4.0, 5.0, 6.0);
665
666 let displacement = b - a;
668 let result = a + displacement;
669 assert!((result.x().value() - b.x().value()).abs() < 1e-12);
670 assert!((result.y().value() - b.y().value()).abs() < 1e-12);
671 assert!((result.z().value() - b.z().value()).abs() < 1e-12);
672 }
673
674 #[test]
675 fn test_position_distance() {
676 let pos = TestPos::new(3.0, 4.0, 0.0);
677 assert!((pos.distance().value() - 5.0).abs() < 1e-12);
678 }
679
680 #[test]
681 fn test_position_direction() {
682 let pos = TestPos::new(3.0, 4.0, 0.0);
683 let dir = pos.direction().expect("non-zero position");
684 let norm = (dir.x() * dir.x() + dir.y() * dir.y() + dir.z() * dir.z()).sqrt();
685 assert!((norm - 1.0).abs() < 1e-12);
686 assert!((dir.x() - 0.6).abs() < 1e-12);
687 assert!((dir.y() - 0.8).abs() < 1e-12);
688 }
689
690 #[test]
691 fn test_position_with_params_and_from_array() {
692 let params = TestParams { id: 42 };
693 let pos = Position::<ParamCenter, TestFrame, Meter>::new_with_params(
694 params.clone(),
695 1.0,
696 2.0,
697 3.0,
698 );
699 assert_eq!(pos.center_params(), ¶ms);
700
701 let arr = [1.0 * M, 2.0 * M, 3.0 * M];
702 let pos_from_arr =
703 Position::<ParamCenter, TestFrame, Meter>::from_array(params.clone(), arr);
704 assert_eq!(pos_from_arr.center_params(), ¶ms);
705 assert!((pos_from_arr.z().value() - 3.0).abs() < 1e-12);
706 }
707
708 #[test]
709 fn test_position_from_array_origin_and_center() {
710 let arr = [1.0 * M, 2.0 * M, 3.0 * M];
711 let pos = Position::<TestCenter, TestFrame, Meter>::from_array_origin(arr);
712 assert!((pos.x().value() - 1.0).abs() < 1e-12);
713
714 let origin = Position::<TestCenter, TestFrame, Meter>::CENTER;
715 assert!(origin.distance().value().abs() < 1e-12);
716 }
717
718 #[test]
719 fn test_position_distance_to_and_sub_methods() {
720 let a = TestPos::new(0.0, 0.0, 0.0);
721 let b = TestPos::new(0.0, 3.0, 4.0);
722 let dist = a.distance_to(&b);
723 assert!((dist.value() - 5.0).abs() < 1e-12);
724
725 let disp: TestDisp = b - a;
726 assert!((disp.y().value() - 3.0).abs() < 1e-12);
727 }
728
729 #[test]
730 fn test_position_direction_unchecked_and_sub_displacement() {
731 let pos = TestPos::new(0.0, 3.0, 4.0);
732 let dir = pos.direction_unchecked();
733 assert!((dir.y() - 0.6).abs() < 1e-12);
734 assert!((dir.z() - 0.8).abs() < 1e-12);
735
736 let disp = TestDisp::new(1.0, 1.0, 1.0);
737 let moved = pos - disp;
738 assert!((moved.y().value() - 2.0).abs() < 1e-12);
739 }
740
741 #[test]
742 fn test_position_spherical_roundtrip() {
743 let pos = TestPos::new(1.0, 1.0, 1.0);
744 let sph = pos.to_spherical();
745 let back = TestPos::from_spherical(&sph);
746 assert!((back.x().value() - pos.x().value()).abs() < 1e-12);
747 assert!((back.y().value() - pos.y().value()).abs() < 1e-12);
748 assert!((back.z().value() - pos.z().value()).abs() < 1e-12);
749 }
750
751 #[test]
752 fn test_position_const_vec3_and_display() {
753 let pos =
754 Position::<TestCenter, TestFrame, Meter>::new_const((), 1.0 * M, 2.0 * M, 3.0 * M);
755 let vec3 = pos.as_array();
756 assert!((vec3[0].value() - 1.0).abs() < 1e-12);
757 assert!((vec3[1].value() - 2.0).abs() < 1e-12);
758 assert!((vec3[2].value() - 3.0).abs() < 1e-12);
759
760 let text = pos.to_string();
761 assert!(text.contains("Center: TestCenter"));
762 assert!(text.contains("Frame: TestFrame"));
763 }
764
765 #[test]
766 fn test_position_display_respects_format_specifiers() {
767 let pos = TestPos::new(1.234_567, -2.0, 3.5);
768
769 let text_prec = format!("{:.2}", pos);
770 let expected_x_prec = format!("{:.2}", pos.x());
771 assert!(text_prec.contains(&format!("X: {expected_x_prec}")));
772
773 let text_exp = format!("{:.3e}", pos);
774 let expected_z_exp = format!("{:.3e}", pos.z());
775 assert!(text_exp.contains(&format!("Z: {expected_z_exp}")));
776 }
777
778 #[test]
779 fn test_position_sub_ref_ref() {
780 let a = TestPos::new(1.0, 2.0, 3.0);
781 let b = TestPos::new(4.0, 6.0, 9.0);
782 let displacement: TestDisp = b - a;
783 assert!((displacement.x().value() - 3.0).abs() < 1e-12);
784 assert!((displacement.y().value() - 4.0).abs() < 1e-12);
785 assert!((displacement.z().value() - 6.0).abs() < 1e-12);
786 }
787
788 type ParamPos = Position<ParamCenter, TestFrame, Meter>;
793
794 #[test]
795 fn test_distance_to_same_params_succeeds() {
796 let params = TestParams { id: 1 };
798 let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
799 let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
800 let d = a.try_distance_to(&b).unwrap();
801 assert!((d.value() - 5.0).abs() < 1e-12);
802 }
803
804 #[test]
805 fn test_distance_to_mismatched_params_err() {
806 let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
810 let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
811 assert!(a.try_distance_to(&b).is_err());
812 }
813
814 #[test]
815 fn test_try_distance_to_same_params_ok() {
816 let params = TestParams { id: 1 };
817 let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
818 let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
819 let result = a.try_distance_to(&b);
820 assert!(result.is_ok());
821 assert!((result.unwrap().value() - 5.0).abs() < 1e-12);
822 }
823
824 #[test]
825 fn test_try_distance_to_mismatched_params_err() {
826 let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
827 let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
828 let result = a.try_distance_to(&b);
829 assert!(result.is_err());
830 let err = result.unwrap_err();
831 assert!(err.to_string().contains("center parameter mismatch"));
832 }
833
834 #[test]
840 fn test_checked_sub_same_params_ok() {
841 let params = TestParams { id: 1 };
842 let a = ParamPos::new_with_params(params.clone(), 0.0, 0.0, 0.0);
843 let b = ParamPos::new_with_params(params, 3.0, 4.0, 0.0);
844 let result = b.checked_sub(&a);
845 assert!(result.is_ok());
846 let disp = result.unwrap();
847 assert!((disp.x().value() - 3.0).abs() < 1e-12);
848 assert!((disp.y().value() - 4.0).abs() < 1e-12);
849 }
850
851 #[test]
852 fn test_checked_sub_mismatched_params_err() {
853 let a = ParamPos::new_with_params(TestParams { id: 1 }, 0.0, 0.0, 0.0);
854 let b = ParamPos::new_with_params(TestParams { id: 2 }, 3.0, 4.0, 0.0);
855 let result = b.checked_sub(&a);
856 assert!(result.is_err());
857 let err = result.unwrap_err();
858 assert!(err.to_string().contains("center parameter mismatch"));
859 }
860
861 #[test]
862 fn test_unit_params_operations_always_succeed() {
863 let a = TestPos::new(0.0, 0.0, 0.0);
865 let b = TestPos::new(3.0, 4.0, 0.0);
866 assert!((a.distance_to(&b).value() - 5.0).abs() < 1e-12);
867 assert!(a.try_distance_to(&b).is_ok());
868 assert!(b.checked_sub(&a).is_ok());
869 }
870
871 #[test]
872 fn test_center_params_mismatch_error_display() {
873 let err = CenterParamsMismatchError {
874 operation: "test_op",
875 };
876 let msg = err.to_string();
877 assert!(msg.contains("test_op"));
878 assert!(msg.contains("center parameter mismatch"));
879
880 let _: &dyn std::error::Error = &err;
882 }
883
884 #[test]
885 fn test_position_to_unit_roundtrip() {
886 let p_m = Position::<TestCenter, TestFrame, Meter>::new(1.0, -0.5, 2.25);
887 let p_km: Position<TestCenter, TestFrame, Kilometer> = p_m.to_unit();
888 let back: Position<TestCenter, TestFrame, Meter> = p_km.to_unit();
889
890 assert!((back.x().value() - p_m.x().value()).abs() < 1e-12);
891 assert!((back.y().value() - p_m.y().value()).abs() < 1e-12);
892 assert!((back.z().value() - p_m.z().value()).abs() < 1e-12);
893 }
894
895 #[test]
896 fn test_position_to_unit_preserves_center_params() {
897 let p_m = ParamPos::new_with_params(TestParams { id: 7 }, 1.0, 2.0, 3.0);
898 let p_km: Position<ParamCenter, TestFrame, Kilometer> = p_m.to_unit();
899
900 assert_eq!(p_km.center_params().id, 7);
901 assert!((p_km.x().value() - 0.001).abs() < 1e-12);
902 assert!((p_km.y().value() - 0.002).abs() < 1e-12);
903 assert!((p_km.z().value() - 0.003).abs() < 1e-12);
904 }
905}