1use core::fmt;
4
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
8pub enum MotionDirection {
9 Direct,
11 Stationary,
13 Retrograde,
15}
16
17impl fmt::Display for MotionDirection {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 let label = match self {
20 Self::Direct => "Direct",
21 Self::Stationary => "Stationary",
22 Self::Retrograde => "Retrograde",
23 };
24 f.write_str(label)
25 }
26}
27
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct Motion {
32 pub longitude_deg_per_day: Option<f64>,
34 pub latitude_deg_per_day: Option<f64>,
36 pub distance_au_per_day: Option<f64>,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq)]
42pub enum MotionValidationError {
43 NonFiniteSpeed {
45 field: &'static str,
47 value: f64,
49 },
50}
51
52impl fmt::Display for MotionValidationError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::NonFiniteSpeed { field, value } => {
56 write!(f, "motion field `{field}` must be finite, got {value}")
57 }
58 }
59 }
60}
61
62impl std::error::Error for MotionValidationError {}
63
64impl Motion {
65 pub const fn new(
67 longitude_deg_per_day: Option<f64>,
68 latitude_deg_per_day: Option<f64>,
69 distance_au_per_day: Option<f64>,
70 ) -> Self {
71 Self {
72 longitude_deg_per_day,
73 latitude_deg_per_day,
74 distance_au_per_day,
75 }
76 }
77
78 pub const fn longitude_speed(self) -> Option<f64> {
80 self.longitude_deg_per_day
81 }
82
83 pub const fn latitude_speed(self) -> Option<f64> {
85 self.latitude_deg_per_day
86 }
87
88 pub const fn distance_speed(self) -> Option<f64> {
90 self.distance_au_per_day
91 }
92
93 pub fn summary_line(self) -> String {
95 let longitude = self
96 .longitude_speed()
97 .map(|value| format!("{value} deg/day"))
98 .unwrap_or_else(|| "n/a".to_string());
99 let latitude = self
100 .latitude_speed()
101 .map(|value| format!("{value} deg/day"))
102 .unwrap_or_else(|| "n/a".to_string());
103 let distance = self
104 .distance_speed()
105 .map(|value| format!("{value} au/day"))
106 .unwrap_or_else(|| "n/a".to_string());
107
108 format!("longitude={longitude}; latitude={latitude}; distance={distance}")
109 }
110
111 pub fn validate(self) -> Result<(), MotionValidationError> {
113 for (field, value) in [
114 ("longitude_deg_per_day", self.longitude_deg_per_day),
115 ("latitude_deg_per_day", self.latitude_deg_per_day),
116 ("distance_au_per_day", self.distance_au_per_day),
117 ] {
118 if let Some(value) = value {
119 if !value.is_finite() {
120 return Err(MotionValidationError::NonFiniteSpeed { field, value });
121 }
122 }
123 }
124
125 Ok(())
126 }
127
128 pub fn longitude_direction(self) -> Option<MotionDirection> {
134 let speed = self.longitude_speed()?;
135 if !speed.is_finite() {
136 return None;
137 }
138
139 Some(if speed > 0.0 {
140 MotionDirection::Direct
141 } else if speed < 0.0 {
142 MotionDirection::Retrograde
143 } else {
144 MotionDirection::Stationary
145 })
146 }
147}
148
149impl fmt::Display for Motion {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.write_str(&self.summary_line())
152 }
153}