doris_rs/
station.rs

1use std::str::FromStr;
2
3#[cfg(doc)]
4use crate::prelude::DORIS;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    constants::USO_FREQ_HZ,
11    prelude::{Matcher, ParsingError, DOMES},
12};
13
14/// [GroundStation] definition, observed from DORIS satellites.
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct GroundStation {
18    /// 4 letter station mnemonic label (antenna point)
19    pub label: String,
20
21    /// Site name
22    pub site: String,
23
24    /// [DOMES] site identifier
25    pub domes: DOMES,
26
27    /// [DORIS] beacon generation
28    pub beacon_revision: u8,
29
30    /// K frequency shift factor
31    pub k_frequency_shift: i8,
32
33    /// ID# used in file indexing
34    pub(crate) code: u16,
35}
36
37impl Default for GroundStation {
38    /// Builds a default [GroundStation] which not suitable as is
39    /// and must be customized.
40    fn default() -> Self {
41        Self {
42            label: Default::default(),
43            site: Default::default(),
44            domes: DOMES::from_str("10003S005").unwrap(),
45            beacon_revision: 3,
46            k_frequency_shift: 0,
47            code: 0,
48        }
49    }
50}
51
52impl GroundStation {
53    /// Defines a [GroundStation] with desired site name
54    pub fn with_site_name(&self, name: &str) -> Self {
55        let mut s = self.clone();
56        s.site = name.to_string();
57        s
58    }
59
60    /// Defines a [GroundStation] with desired site label,
61    /// which should be a 4 letter description of the site name.
62    pub fn with_site_label(&self, label: &str) -> Self {
63        let mut s = self.clone();
64        s.label = label.to_string();
65        s
66    }
67
68    /// Defines a [GroundStation] with desired [DOMES] site number
69    pub fn with_domes_str(&self, domes: &str) -> Result<Self, ParsingError> {
70        let domes = DOMES::from_str(domes)?;
71        Ok(self.with_domes(domes))
72    }
73
74    /// Defines a [GroundStation] with desired [DOMES] site number
75    pub fn with_domes(&self, domes: DOMES) -> Self {
76        let mut s = self.clone();
77        s.domes = domes;
78        s
79    }
80
81    /// Returns [GroundStation] with updated DORIS beacon revision
82    pub fn with_beacon_revision(&self, revision: u8) -> Self {
83        let mut s = self.clone();
84        s.beacon_revision = revision;
85        s
86    }
87
88    /// Defines a [GroundStation] with updated f1/f2 frequency shift
89    pub fn with_frequency_shift(&self, shift: i8) -> Self {
90        let mut s = self.clone();
91        s.k_frequency_shift = shift;
92        s
93    }
94
95    /// Defines a [GroundStation] with desired station ID#
96    /// which is used to define this site uniquely in a DORIS file.
97    pub fn with_unique_id(&self, code: u16) -> Self {
98        let mut s = self.clone();
99        s.code = code;
100        s
101    }
102
103    /// Returns true if this [GroundStation] is matched by given [Matcher] specs
104    pub fn matches<'a>(&self, matcher: &'a Matcher) -> bool {
105        match matcher {
106            Matcher::ID(code) => self.code == *code,
107            Matcher::Site(site) => self.site.to_uppercase() == site.to_uppercase(),
108            Matcher::DOMES(domes) => self.domes == *domes,
109            Matcher::Label(label) => self.label.to_uppercase() == label.to_uppercase(),
110        }
111    }
112
113    /// Returns S1 frequency shift for this [GroundStation] in Hertz
114    pub fn s1_frequency_shift(&self) -> f64 {
115        543.0
116            * USO_FREQ_HZ
117            * (3.0 / 4.0 + 87.0 * self.k_frequency_shift as f64 / 5.0 * 2.0_f64.powi(26))
118    }
119
120    /// Returns U2 frequency shift for this [GroundStation] in Hertz
121    pub fn u2_frequency_shift(&self) -> f64 {
122        107.0
123            * USO_FREQ_HZ
124            * (3.0 / 4.0 + 87.0 * self.k_frequency_shift as f64 / 5.0 * 2.0_f64.powi(26))
125    }
126}
127
128impl std::str::FromStr for GroundStation {
129    type Err = ParsingError;
130    fn from_str(content: &str) -> Result<Self, Self::Err> {
131        if content.len() < 40 {
132            return Err(ParsingError::GroundStation);
133        }
134
135        let content = content.split_at(1).1;
136        let (key, rem) = content.split_at(4);
137        let (label, rem) = rem.split_at(5);
138        let (name, rem) = rem.split_at(30);
139        let (domes, rem) = rem.split_at(10);
140        let (gen, rem) = rem.split_at(3);
141        let (k_factor, _) = rem.split_at(3);
142
143        Ok(GroundStation {
144            site: name.trim().to_string(),
145            label: label.trim().to_string(),
146            domes: DOMES::from_str(domes.trim())?,
147            beacon_revision: gen
148                .trim()
149                .parse::<u8>()
150                .map_err(|_| ParsingError::GroundStation)?,
151            k_frequency_shift: k_factor
152                .trim()
153                .parse::<i8>()
154                .map_err(|_| ParsingError::GroundStation)?,
155            code: key
156                .trim()
157                .parse::<u16>()
158                .map_err(|_| ParsingError::GroundStation)?,
159        })
160    }
161}
162
163impl std::fmt::Display for GroundStation {
164    /// Formats [GroundStation] verbosely
165    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
166        write!(
167            f,
168            "Station {} ({}/{}) (rev={}) (freq={})",
169            self.label, self.site, self.domes, self.beacon_revision, self.k_frequency_shift
170        )
171    }
172}
173
174impl std::fmt::LowerHex for GroundStation {
175    /// Formats [GroundStation] according to DORIS standards
176    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177        write!(
178            f,
179            "D{:02}  {} {:<29} {}  {} {:3}",
180            self.code,
181            self.label,
182            self.site,
183            self.domes,
184            self.beacon_revision,
185            self.k_frequency_shift
186        )
187    }
188}
189
190#[cfg(test)]
191mod test {
192    use super::GroundStation;
193    use crate::prelude::{DOMESTrackingPoint, DOMES};
194    use std::str::FromStr;
195
196    #[test]
197    fn default_station() {
198        let _ = GroundStation::default();
199    }
200
201    #[test]
202    fn station_parsing() {
203        for (desc, expected) in [
204            (
205                "D01  OWFC OWENGA                        50253S002  3   0",
206                GroundStation {
207                    label: "OWFC".to_string(),
208                    site: "OWENGA".to_string(),
209                    domes: DOMES {
210                        area: 502,
211                        site: 53,
212                        sequential: 2,
213                        point: DOMESTrackingPoint::Instrument,
214                    },
215                    beacon_revision: 3,
216                    k_frequency_shift: 0,
217                    code: 1,
218                },
219            ),
220            (
221                "D17  GRFB GREENBELT                     40451S178  3   0",
222                GroundStation {
223                    label: "GRFB".to_string(),
224                    site: "GREENBELT".to_string(),
225                    domes: DOMES {
226                        area: 404,
227                        site: 51,
228                        sequential: 178,
229                        point: DOMESTrackingPoint::Instrument,
230                    },
231                    beacon_revision: 3,
232                    k_frequency_shift: 0,
233                    code: 17,
234                },
235            ),
236            (
237                "D12  GR4B GRASSE                        10002S019  3 -15",
238                GroundStation {
239                    label: "GR4B".to_string(),
240                    site: "GRASSE".to_string(),
241                    domes: DOMES {
242                        area: 100,
243                        site: 02,
244                        sequential: 19,
245                        point: DOMESTrackingPoint::Instrument,
246                    },
247                    beacon_revision: 3,
248                    k_frequency_shift: -15,
249                    code: 12,
250                },
251            ),
252        ] {
253            let station = GroundStation::from_str(desc).unwrap();
254            assert_eq!(station, expected, "station parsing error");
255
256            let formatted = format!("{:x}", station);
257            assert_eq!(formatted, desc, "station reciprocal error");
258        }
259    }
260}