gnss_rs/
sv.rs

1//! Space vehicle definition
2use hifitime::{Epoch, TimeScale};
3use thiserror::Error;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use crate::constellation::{Constellation, ParsingError as ConstellationParsingError};
9
10use std::num::ParseIntError;
11
12use std::str::FromStr;
13
14/// ̀SV describes a Satellite Vehicle
15#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct SV {
18    /// PRN identification # for this vehicle
19    pub prn: u8,
20    /// `GNSS` Constellation to which this vehicle belongs to
21    pub constellation: Constellation,
22}
23
24/*
25 * Database, built by build.rs, for detailed SBAS vehicle identification
26 */
27include!(concat!(env!("OUT_DIR"), "/sbas.rs"));
28
29/// ̀[SV] parsing related issues.
30#[derive(Error, Debug, Clone, PartialEq)]
31pub enum ParsingError {
32    #[error("constellation parsing error: {0}")]
33    ConstellationParsing(#[from] ConstellationParsingError),
34
35    #[error("satellite number parsing error: {0}")]
36    PRNParsing(#[from] ParseIntError),
37}
38
39impl SV {
40    /// Builds desired Satellite Vehicle ([SV]) regardless of the
41    /// both field values. Prefer [Self::new_sbas] to conveniently
42    /// idenfity geostationnary vehicles.
43    /// ```
44    /// use gnss_rs::sv;
45    /// use gnss_rs::prelude::*;
46    ///
47    /// use std::str::FromStr;
48    /// use hifitime::{TimeScale, Epoch};
49    ///
50    /// // This method lets you construct satellites that may not exist
51    /// let sv = SV::new(Constellation::GPS, 1);
52    ///
53    /// assert_eq!(sv.constellation, Constellation::GPS);
54    /// assert_eq!(sv.prn, 1);
55    /// assert_eq!(sv.launch_date(), None); // only for SBAS vehicles
56    /// ```
57    pub const fn new(constellation: Constellation, prn: u8) -> Self {
58        Self { prn, constellation }
59    }
60
61    /// Tries to identify this [Constellation::SBAS] satellite from
62    /// a PRN number ranging from 0..100 (RINEX like format).
63    /// Simply substract 100 to the true satellite ID number.
64    ///
65    /// ```
66    /// use gnss_rs::sv;
67    /// use gnss_rs::prelude::*;
68    ///
69    /// use std::str::FromStr;
70    /// use hifitime::{TimeScale, Epoch, MonthName};
71    ///
72    /// // This only works if satellite do exist in our database
73    /// assert!(SV::new_sbas(1).is_none());
74    ///
75    /// let egnos_geo23 = SV::new_sbas(23)
76    ///     .unwrap(); // GEO #123
77    ///
78    /// assert_eq!(egnos_geo23.prn, 23);
79    /// assert!(egnos_geo23.constellation.is_sbas()); // obviously
80    /// assert_eq!(egnos_geo23.constellation, Constellation::EGNOS); // smart builder
81    ///
82    /// let launch_date = egnos_geo23.launch_date()
83    ///     .unwrap(); // only for detailed SBAS
84    ///
85    /// assert_eq!(launch_date.year(), 2021);
86    /// assert_eq!(launch_date.month_name(), MonthName::November);
87    /// ```
88    pub fn new_sbas(prn: u8) -> Option<Self> {
89        let definition = Self::sbas_definitions(prn)?;
90
91        if let Ok(constellation) = Constellation::from_str(definition.constellation) {
92            Some(Self { prn, constellation })
93        } else {
94            None
95        }
96    }
97
98    /// Returns [Timescale] to which [Self] belongs to.
99    /// ```
100    /// extern crate gnss_rs as gnss;
101    ///
102    /// use hifitime::TimeScale;
103    /// use gnss::sv;
104    /// use gnss::prelude::*;
105    /// use std::str::FromStr;
106    ///
107    /// assert_eq!(sv!("G01").timescale(), Some(TimeScale::GPST));
108    /// assert_eq!(sv!("E13").timescale(), Some(TimeScale::GST));
109    /// ```
110    pub fn timescale(&self) -> Option<TimeScale> {
111        self.constellation.timescale()
112    }
113
114    /// Explores SBAS detail database and tries to provide more detail from unique
115    /// PRN number (+100).
116    pub(crate) fn sbas_definitions(prn: u8) -> Option<&'static SBASHelper<'static>> {
117        let to_find = (prn as u16) + 100;
118        SBAS_VEHICLES
119            .iter()
120            .filter(|e| e.prn == to_find)
121            .reduce(|e, _| e)
122    }
123
124    /// Returns launch date and time expressed as UTC [Epoch].  
125    /// This API is limited to [Constellation::SBAS] vehicles for which we store more details.
126    pub fn launch_date(&self) -> Option<Epoch> {
127        let definition = SV::sbas_definitions(self.prn)?;
128
129        Some(Epoch::from_gregorian_utc_at_midnight(
130            definition.launch_year,
131            definition.launch_month,
132            definition.launch_day,
133        ))
134    }
135
136    /// Returns True if [Self] is a [Constellation::BeiDou] geostationnary vehicle
137    pub fn is_beidou_geo(&self) -> bool {
138        self.constellation == Constellation::BeiDou && (self.prn < 6 || self.prn > 58)
139    }
140}
141
142impl std::str::FromStr for SV {
143    type Err = ParsingError;
144    /*
145     * Parse SV from "XYY" standardized format.
146     * On "sbas" crate feature, we have the ability to identify
147     * vehicles in detail. For example S23 is Eutelsat 5WB.
148     */
149    fn from_str(string: &str) -> Result<Self, Self::Err> {
150        let constellation = Constellation::from_str(&string[0..1])?;
151        let prn = string[1..].trim().parse::<u8>()?;
152        let mut ret = SV::new(constellation, prn);
153        if constellation.is_sbas() {
154            // map the SXX to meaningful SBAS
155            if let Some(sbas) = SV::sbas_definitions(prn) {
156                // this can't fail because the SBAS database only
157                // contains valid Constellations
158                ret.constellation = Constellation::from_str(sbas.constellation).unwrap();
159            }
160        }
161        Ok(ret)
162    }
163}
164
165impl std::fmt::UpperHex for SV {
166    /*
167     * Possibly detailed identity for SBAS vehicles
168     */
169    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
170        if self.constellation.is_sbas() {
171            if let Some(sbas) = SV::sbas_definitions(self.prn) {
172                write!(f, "{}", sbas.id)
173            } else {
174                write!(f, "{:x}", self)
175            }
176        } else {
177            write!(f, "{:x}", self)
178        }
179    }
180}
181
182impl std::fmt::LowerHex for SV {
183    /*
184     * Prints self as XYY standard format
185     */
186    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
187        write!(f, "{:x}{:02}", self.constellation, self.prn)
188    }
189}
190
191impl std::fmt::Display for SV {
192    /*
193     * Prints self as XYY standard format
194     */
195    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
196        write!(f, "{:x}{:02}", self.constellation, self.prn)
197    }
198}
199
200#[cfg(test)]
201mod test {
202    use super::*;
203    use std::str::FromStr;
204    #[test]
205    fn from_str() {
206        for (descriptor, expected) in vec![
207            ("G01", SV::new(Constellation::GPS, 1)),
208            ("G 1", SV::new(Constellation::GPS, 1)),
209            ("G33", SV::new(Constellation::GPS, 33)),
210            ("C01", SV::new(Constellation::BeiDou, 1)),
211            ("C 3", SV::new(Constellation::BeiDou, 3)),
212            ("R01", SV::new(Constellation::Glonass, 1)),
213            ("R 1", SV::new(Constellation::Glonass, 1)),
214            ("C254", SV::new(Constellation::BeiDou, 254)),
215            ("E4 ", SV::new(Constellation::Galileo, 4)),
216            ("R 9", SV::new(Constellation::Glonass, 9)),
217            ("I 3", SV::new(Constellation::IRNSS, 3)),
218            ("I09", SV::new(Constellation::IRNSS, 9)),
219            ("I16", SV::new(Constellation::IRNSS, 16)),
220        ] {
221            let sv = SV::from_str(descriptor);
222            assert!(
223                sv.is_ok(),
224                "failed to parse sv from \"{}\" - {:?}",
225                descriptor,
226                sv.err().unwrap()
227            );
228            let sv = sv.unwrap();
229            assert_eq!(
230                sv, expected,
231                "badly identified {} from \"{}\"",
232                sv, descriptor
233            );
234        }
235    }
236    #[test]
237    fn sbas_from_str() {
238        for (desc, parsed, lowerhex, upperhex) in vec![
239            ("S 3", SV::new(Constellation::SBAS, 3), "S03", "S03"),
240            (
241                "S22",
242                SV::new(Constellation::AusNZ, 22),
243                "S22",
244                "INMARSAT-4F1",
245            ),
246            ("S23", SV::new(Constellation::EGNOS, 23), "S23", "ASTRA-5B"),
247            ("S25", SV::new(Constellation::SDCM, 25), "S25", "Luch-5A"),
248            ("S 5", SV::new(Constellation::SBAS, 5), "S05", "S05"),
249            ("S48", SV::new(Constellation::ASAL, 48), "S48", "ALCOMSAT-1"),
250        ] {
251            let sv = SV::from_str(desc).unwrap();
252            assert_eq!(sv, parsed, "failed to parse correct sv from \"{}\"", desc);
253            assert_eq!(format!("{:x}", sv), lowerhex);
254            assert_eq!(format!("{:X}", sv), upperhex);
255            assert!(sv.constellation.is_sbas(), "should be sbas");
256        }
257    }
258    #[test]
259    fn sbas_db_sanity() {
260        for sbas in SBAS_VEHICLES.iter() {
261            /* verify PRN */
262            assert!(sbas.prn > 100);
263
264            /* verify constellation */
265            let constellation = Constellation::from_str(sbas.constellation);
266            assert!(
267                constellation.is_ok(),
268                "sbas database should only contain valid constellations: \"{}\"",
269                sbas.constellation,
270            );
271
272            let constellation = constellation.unwrap();
273            assert_eq!(constellation.timescale(), Some(TimeScale::GPST));
274
275            /* verify launch date */
276            let _ = Epoch::from_gregorian_utc_at_midnight(
277                sbas.launch_year,
278                sbas.launch_month,
279                sbas.launch_day,
280            );
281        }
282    }
283    #[test]
284    fn test_beidou_geo() {
285        assert_eq!(SV::from_str("G01").unwrap().is_beidou_geo(), false);
286        assert_eq!(SV::from_str("E01").unwrap().is_beidou_geo(), false);
287        assert_eq!(SV::from_str("C01").unwrap().is_beidou_geo(), true);
288        assert_eq!(SV::from_str("C02").unwrap().is_beidou_geo(), true);
289        assert_eq!(SV::from_str("C06").unwrap().is_beidou_geo(), false);
290        assert_eq!(SV::from_str("C48").unwrap().is_beidou_geo(), false);
291        assert_eq!(SV::from_str("C59").unwrap().is_beidou_geo(), true);
292        assert_eq!(SV::from_str("C60").unwrap().is_beidou_geo(), true);
293    }
294}