1use crate::constellation::Constellation;
3use hifitime::{Epoch, TimeScale};
4use thiserror::Error;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct SV {
13 pub prn: u8,
15 pub constellation: Constellation,
17}
18
19include!(concat!(env!("OUT_DIR"), "/sbas.rs"));
23
24#[derive(Error, Debug, Clone, PartialEq)]
26pub enum ParsingError {
27 #[error("constellation parsing error")]
28 ConstellationParsing(#[from] crate::constellation::ParsingError),
29 #[error("sv prn# parsing error")]
30 PRNParsing(#[from] std::num::ParseIntError),
31}
32
33impl SV {
34 pub const fn new(constellation: Constellation, prn: u8) -> Self {
56 Self { prn, constellation }
57 }
58 pub fn timescale(&self) -> Option<TimeScale> {
71 self.constellation.timescale()
72 }
73 pub(crate) fn sbas_definitions(prn: u8) -> Option<&'static SBASHelper<'static>> {
79 let to_find = (prn as u16) + 100;
80 SBAS_VEHICLES
81 .iter()
82 .filter(|e| e.prn == to_find)
83 .reduce(|e, _| e)
84 }
85 pub fn launched_date(&self) -> Option<Epoch> {
88 let definition = SV::sbas_definitions(self.prn)?;
89 Some(Epoch::from_gregorian_utc_at_midnight(
90 definition.launched_year,
91 definition.launched_month,
92 definition.launched_day,
93 ))
94 }
95 pub fn is_beidou_geo(&self) -> bool {
97 self.constellation == Constellation::BeiDou && (self.prn < 6 || self.prn > 58)
98 }
99}
100
101impl std::str::FromStr for SV {
102 type Err = ParsingError;
103 fn from_str(string: &str) -> Result<Self, Self::Err> {
109 let constellation = Constellation::from_str(&string[0..1])?;
110 let prn = string[1..].trim().parse::<u8>()?;
111 let mut ret = SV::new(constellation, prn);
112 if constellation.is_sbas() {
113 if let Some(sbas) = SV::sbas_definitions(prn) {
115 ret.constellation = Constellation::from_str(sbas.constellation).unwrap();
118 }
119 }
120 Ok(ret)
121 }
122}
123
124impl std::fmt::UpperHex for SV {
125 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
129 if self.constellation.is_sbas() {
130 if let Some(sbas) = SV::sbas_definitions(self.prn) {
131 write!(f, "{}", sbas.id)
132 } else {
133 write!(f, "{:x}", self)
134 }
135 } else {
136 write!(f, "{:x}", self)
137 }
138 }
139}
140
141impl std::fmt::LowerHex for SV {
142 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
146 write!(f, "{:x}{:02}", self.constellation, self.prn)
147 }
148}
149
150impl std::fmt::Display for SV {
151 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
155 write!(f, "{:x}{:02}", self.constellation, self.prn)
156 }
157}
158
159#[cfg(test)]
160mod test {
161 use super::*;
162 use std::str::FromStr;
163 #[test]
164 fn from_str() {
165 for (descriptor, expected) in vec![
166 ("G01", SV::new(Constellation::GPS, 1)),
167 ("G 1", SV::new(Constellation::GPS, 1)),
168 ("G33", SV::new(Constellation::GPS, 33)),
169 ("C01", SV::new(Constellation::BeiDou, 1)),
170 ("C 3", SV::new(Constellation::BeiDou, 3)),
171 ("R01", SV::new(Constellation::Glonass, 1)),
172 ("R 1", SV::new(Constellation::Glonass, 1)),
173 ("C254", SV::new(Constellation::BeiDou, 254)),
174 ("E4 ", SV::new(Constellation::Galileo, 4)),
175 ("R 9", SV::new(Constellation::Glonass, 9)),
176 ("I 3", SV::new(Constellation::IRNSS, 3)),
177 ("I09", SV::new(Constellation::IRNSS, 9)),
178 ("I16", SV::new(Constellation::IRNSS, 16)),
179 ] {
180 let sv = SV::from_str(descriptor);
181 assert!(
182 sv.is_ok(),
183 "failed to parse sv from \"{}\" - {:?}",
184 descriptor,
185 sv.err().unwrap()
186 );
187 let sv = sv.unwrap();
188 assert_eq!(
189 sv, expected,
190 "badly identified {} from \"{}\"",
191 sv, descriptor
192 );
193 }
194 }
195 #[test]
196 fn sbas_from_str() {
197 for (desc, parsed, lowerhex, upperhex) in vec![
198 ("S 3", SV::new(Constellation::SBAS, 3), "S03", "S03"),
199 (
200 "S22",
201 SV::new(Constellation::AusNZ, 22),
202 "S22",
203 "INMARSAT-4F1",
204 ),
205 ("S23", SV::new(Constellation::EGNOS, 23), "S23", "ASTRA-5B"),
206 ("S25", SV::new(Constellation::SDCM, 25), "S25", "Luch-5A"),
207 ("S 5", SV::new(Constellation::SBAS, 5), "S05", "S05"),
208 ("S48", SV::new(Constellation::ASAL, 48), "S48", "ALCOMSAT-1"),
209 ] {
210 let sv = SV::from_str(desc).unwrap();
211 assert_eq!(sv, parsed, "failed to parse correct sv from \"{}\"", desc);
212 assert_eq!(format!("{:x}", sv), lowerhex);
213 assert_eq!(format!("{:X}", sv), upperhex);
214 assert!(sv.constellation.is_sbas(), "should be sbas");
215 }
216 }
217 #[test]
218 fn sbas_db_sanity() {
219 for sbas in SBAS_VEHICLES.iter() {
220 assert!(sbas.prn > 100);
222
223 let constellation = Constellation::from_str(sbas.constellation);
225 assert!(
226 constellation.is_ok(),
227 "sbas database should only contain valid constellations: \"{}\"",
228 sbas.constellation,
229 );
230
231 let constellation = constellation.unwrap();
232 assert_eq!(constellation.timescale(), Some(TimeScale::GPST));
233
234 let _ = Epoch::from_gregorian_utc_at_midnight(
236 sbas.launched_year,
237 sbas.launched_month,
238 sbas.launched_day,
239 );
240 }
241 }
242 #[test]
243 fn test_beidou_geo() {
244 assert_eq!(SV::from_str("G01").unwrap().is_beidou_geo(), false);
245 assert_eq!(SV::from_str("E01").unwrap().is_beidou_geo(), false);
246 assert_eq!(SV::from_str("C01").unwrap().is_beidou_geo(), true);
247 assert_eq!(SV::from_str("C02").unwrap().is_beidou_geo(), true);
248 assert_eq!(SV::from_str("C06").unwrap().is_beidou_geo(), false);
249 assert_eq!(SV::from_str("C48").unwrap().is_beidou_geo(), false);
250 assert_eq!(SV::from_str("C59").unwrap().is_beidou_geo(), true);
251 assert_eq!(SV::from_str("C60").unwrap().is_beidou_geo(), true);
252 }
253}