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 == *site,
108            Matcher::DOMES(domes) => self.domes == *domes,
109            Matcher::Label(label) => self.label == *label,
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    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
165        write!(
166            f,
167            "D{:02}  {} {:<29} {}  {}   {}",
168            self.code,
169            self.label,
170            self.site,
171            self.domes,
172            self.beacon_revision,
173            self.k_frequency_shift
174        )
175    }
176}
177
178#[cfg(test)]
179mod test {
180    use super::GroundStation;
181    use crate::prelude::{DOMESTrackingPoint, DOMES};
182    use std::str::FromStr;
183
184    #[test]
185    fn default_station() {
186        let _ = GroundStation::default();
187    }
188
189    #[test]
190    fn station_parsing() {
191        for (desc, expected) in [
192            (
193                "D01  OWFC OWENGA                        50253S002  3   0",
194                GroundStation {
195                    label: "OWFC".to_string(),
196                    site: "OWENGA".to_string(),
197                    domes: DOMES {
198                        area: 502,
199                        site: 53,
200                        sequential: 2,
201                        point: DOMESTrackingPoint::Instrument,
202                    },
203                    beacon_revision: 3,
204                    k_frequency_shift: 0,
205                    code: 1,
206                },
207            ),
208            (
209                "D17  GRFB GREENBELT                     40451S178  3   0",
210                GroundStation {
211                    label: "GRFB".to_string(),
212                    site: "GREENBELT".to_string(),
213                    domes: DOMES {
214                        area: 404,
215                        site: 51,
216                        sequential: 178,
217                        point: DOMESTrackingPoint::Instrument,
218                    },
219                    beacon_revision: 3,
220                    k_frequency_shift: 0,
221                    code: 17,
222                },
223            ),
224        ] {
225            let station = GroundStation::from_str(desc).unwrap();
226            assert_eq!(station, expected, "station parsing error");
227            assert_eq!(station.to_string(), desc, "station reciprocal error");
228        }
229    }
230}