rinex/navigation/ephemeris/
orbits.rs

1//! NAV Orbits description, spanning all revisions and constellations
2use std::str::FromStr;
3
4use crate::{
5    navigation::ephemeris::flags::{
6        bds::{
7            BdsB1cIntegrity, BdsB2aB1cIntegrity, BdsB2bIntegrity, BdsHealth, BdsSatH1,
8            BdsSatelliteType,
9        },
10        gal::{GalDataSource, GalHealth},
11        geo::GeoHealth,
12        glonass::{GlonassHealth, GlonassHealth2, GlonassStatus},
13        gps::{GpsQzssl1cHealth, GpsQzssl1l2l5Health},
14        irnss::IrnssHealth,
15    },
16    prelude::ParsingError,
17};
18
19#[cfg(feature = "serde")]
20use serde::Serialize;
21
22include!(concat!(env!("OUT_DIR"), "/nav_orbits.rs"));
23
24/// [OrbitItem] item is Navigation ephemeris entry.
25/// It is a complex data wrapper, for high level
26/// record description, across all revisions and constellations
27#[derive(Debug, Clone, PartialEq, PartialOrd)]
28#[cfg_attr(feature = "serde", derive(Serialize))]
29pub enum OrbitItem {
30    /// Interpreted as unsigned byte
31    U8(u8),
32
33    /// Interpreted as signed byte
34    I8(i8),
35
36    /// Interpreted as unsigned 32 bit value
37    U32(u32),
38
39    /// Interpreted as double precision data
40    F64(f64),
41
42    /// L2P flag from GPS message is asserted when
43    /// the quadrature L2 P(Y) does stream data
44    /// otherwise it has been commanded off, most likely under A/S.
45    Gpsl2pFlag(bool),
46
47    /// GPS / QZSS [GpsQzssl1cHealth] flag
48    GpsQzssl1cHealth(GpsQzssl1cHealth),
49
50    /// GPS / QZSS [GpsQzssl1l2l5Health] flag
51    GpsQzssl1l2l5Health(GpsQzssl1l2l5Health),
52
53    /// [GeoHealth] SV indication
54    GeoHealth(GeoHealth),
55
56    /// [GalHealth] SV indication
57    GalHealth(GalHealth),
58
59    /// [GalDataSource] signal indication
60    GalDataSource(GalDataSource),
61
62    /// [IrnssHealth] SV indication
63    IrnssHealth(IrnssHealth),
64
65    /// [GlonassHealth] SV indication
66    GlonassHealth(GlonassHealth),
67
68    /// [GlonassHealth2] SV indication present in modern frames
69    GlonassHealth2(GlonassHealth2),
70
71    /// [GlonassStatus] NAV4 Orbit7 status flag
72    GlonassStatus(GlonassStatus),
73
74    /// [BdsSatH1] health flag in historical and D1/D2 frames
75    BdsSatH1(BdsSatH1),
76
77    /// [BdsHealth] flag in modern frames
78    BdsHealth(BdsHealth),
79
80    /// [BdsSatelliteType] indication
81    BdsSatelliteType(BdsSatelliteType),
82
83    /// [BdsB1cIntegrity] flag
84    BdsB1cIntegrity(BdsB1cIntegrity),
85
86    /// [BdsB2aB1cIntegrity] flag
87    BdsB2aB1cIntegrity(BdsB2aB1cIntegrity),
88
89    /// [BdsB2bIntegrity] flag
90    BdsB2bIntegrity(BdsB2bIntegrity),
91}
92
93impl std::fmt::Display for OrbitItem {
94    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
95        match self {
96            Self::U8(val) => write!(f, "{:02x}", val),
97            Self::I8(val) => write!(f, "{:02x}", val),
98            Self::U32(val) => write!(f, "{:08X}", val),
99            Self::F64(val) => write!(f, "{}", val),
100            Self::Gpsl2pFlag(val) => write!(f, "l2p={:?}", val),
101            Self::GeoHealth(val) => write!(f, "{:?}", val),
102            Self::GalHealth(val) => write!(f, "{:?}", val),
103            Self::GalDataSource(val) => write!(f, "{:?}", val),
104            Self::IrnssHealth(val) => write!(f, "{:?}", val),
105            Self::GlonassHealth(val) => write!(f, "{:?}", val),
106            Self::GlonassStatus(val) => write!(f, "{:?}", val),
107            Self::BdsSatH1(val) => write!(f, "{:?}", val),
108            Self::BdsHealth(val) => write!(f, "{:?}", val),
109            Self::BdsSatelliteType(val) => write!(f, "{:?}", val),
110            Self::GlonassHealth2(val) => write!(f, "{:?}", val),
111            Self::GpsQzssl1cHealth(val) => write!(f, "{:?}", val),
112            Self::GpsQzssl1l2l5Health(val) => write!(f, "{:?}", val),
113            Self::BdsB1cIntegrity(val) => write!(f, "{:?}", val),
114            Self::BdsB2aB1cIntegrity(val) => write!(f, "{:?}", val),
115            Self::BdsB2bIntegrity(val) => write!(f, "{:?}", val),
116        }
117    }
118}
119
120impl From<u32> for OrbitItem {
121    fn from(value: u32) -> Self {
122        Self::U32(value)
123    }
124}
125
126impl From<f64> for OrbitItem {
127    fn from(value: f64) -> Self {
128        Self::F64(value)
129    }
130}
131
132impl OrbitItem {
133    /// Builds a [OrbitItem] from type descriptor and string content
134    pub fn new(
135        name_str: &str,
136        type_str: &str,
137        val_str: &str,
138        msgtype: &NavMessageType,
139        constellation: Constellation,
140    ) -> Result<OrbitItem, ParsingError> {
141        // make it "rust" compatible
142        let float =
143            f64::from_str(&val_str.replace('D', "e")).map_err(|_| ParsingError::NavNullOrbit)?;
144
145        // do not tolerate zero values for native types
146        match type_str {
147            "u8" | "i8" | "u32" | "f64" => {
148                if float == 0.0 {
149                    return Err(ParsingError::NavNullOrbit);
150                }
151            },
152            _ => {}, // non-native types
153        }
154
155        // uninterpreted data remains as native type and we exit.
156        match type_str {
157            "u8" => {
158                let unsigned = float.round() as u8;
159                return Ok(OrbitItem::U8(unsigned));
160            },
161
162            "i8" => {
163                let signed = float.round() as i8;
164                return Ok(OrbitItem::I8(signed));
165            },
166
167            "u32" => {
168                let unsigned = float.round() as u32;
169                return Ok(OrbitItem::U32(unsigned));
170            },
171
172            "f64" => {
173                return Ok(OrbitItem::F64(float));
174            },
175            _ => {},
176        };
177
178        // handle complex types
179        match type_str {
180            "flag" => {
181                // bit flags interpretation
182                let unsigned = float.round() as u32;
183
184                match name_str {
185                    "health" => {
186                        // complex health flag interpretation
187
188                        // handle GEO case
189                        if constellation.is_sbas() {
190                            match msgtype {
191                                NavMessageType::LNAV | NavMessageType::SBAS => {
192                                    let flags = GeoHealth::from_bits(unsigned)
193                                        .ok_or(ParsingError::NavFlagsMapping)?;
194
195                                    return Ok(OrbitItem::GeoHealth(flags));
196                                },
197                                _ => return Err(ParsingError::NavHealthFlagDefinition),
198                            }
199                        }
200
201                        // other cases
202                        match (msgtype, constellation) {
203                            (
204                                NavMessageType::LNAV | NavMessageType::CNAV,
205                                Constellation::GPS | Constellation::QZSS,
206                            ) => {
207                                let flags = GpsQzssl1l2l5Health::from(unsigned);
208
209                                Ok(OrbitItem::GpsQzssl1l2l5Health(flags))
210                            },
211                            (NavMessageType::CNV2, Constellation::GPS | Constellation::QZSS) => {
212                                let flags = GpsQzssl1cHealth::from_bits(unsigned)
213                                    .ok_or(ParsingError::NavFlagsMapping)?;
214
215                                Ok(OrbitItem::GpsQzssl1cHealth(flags))
216                            },
217                            (
218                                NavMessageType::LNAV | NavMessageType::INAV | NavMessageType::FNAV,
219                                Constellation::Galileo,
220                            ) => {
221                                let flags = GalHealth::from_bits(unsigned)
222                                    .ok_or(ParsingError::NavFlagsMapping)?;
223
224                                Ok(OrbitItem::GalHealth(flags))
225                            },
226                            (
227                                NavMessageType::LNAV | NavMessageType::FDMA,
228                                Constellation::Glonass,
229                            ) => {
230                                let flags = GlonassHealth::from_bits(unsigned)
231                                    .ok_or(ParsingError::NavFlagsMapping)?;
232
233                                Ok(OrbitItem::GlonassHealth(flags))
234                            },
235                            (
236                                NavMessageType::LNAV | NavMessageType::D1 | NavMessageType::D2,
237                                Constellation::BeiDou,
238                            ) => {
239                                // BDS H1 flag
240                                let flags = BdsSatH1::from_bits(unsigned)
241                                    .ok_or(ParsingError::NavFlagsMapping)?;
242
243                                Ok(OrbitItem::BdsSatH1(flags))
244                            },
245                            (
246                                NavMessageType::CNV1 | NavMessageType::CNV2 | NavMessageType::CNV3,
247                                Constellation::BeiDou,
248                            ) => {
249                                // Modern BDS flag
250                                let flags = BdsHealth::from(unsigned);
251
252                                Ok(OrbitItem::BdsHealth(flags))
253                            },
254                            _ => Err(ParsingError::NavHealthFlagDefinition),
255                        }
256                    },
257                    "health2" => {
258                        // Subsidary health flags
259                        match (msgtype, constellation) {
260                            (NavMessageType::FDMA, Constellation::Glonass) => {
261                                let flags = GlonassHealth2::from_bits(unsigned)
262                                    .ok_or(ParsingError::NavFlagsMapping)?;
263
264                                Ok(OrbitItem::GlonassHealth2(flags))
265                            },
266                            _ => Err(ParsingError::NavHealthFlagDefinition),
267                        }
268                    },
269                    "source" => {
270                        // complex signal source indication
271                        match (msgtype, constellation) {
272                            (
273                                NavMessageType::LNAV | NavMessageType::INAV | NavMessageType::FNAV,
274                                Constellation::Galileo,
275                            ) => {
276                                let flags = GalDataSource::from_bits(unsigned)
277                                    .ok_or(ParsingError::NavFlagsMapping)?;
278
279                                Ok(OrbitItem::GalDataSource(flags))
280                            },
281                            _ => Err(ParsingError::NavDataSourceDefinition),
282                        }
283                    },
284                    "satType" => match (msgtype, constellation) {
285                        (NavMessageType::CNV1 | NavMessageType::CNV2, Constellation::BeiDou) => {
286                            let flags = BdsSatelliteType::from(unsigned);
287
288                            Ok(OrbitItem::BdsSatelliteType(flags))
289                        },
290                        _ => Err(ParsingError::NavDataSourceDefinition),
291                    },
292                    "integrity" => match (msgtype, constellation) {
293                        (NavMessageType::CNV1, Constellation::BeiDou) => {
294                            let flags = BdsB1cIntegrity::from_bits(unsigned)
295                                .ok_or(ParsingError::NavFlagsMapping)?;
296
297                            Ok(OrbitItem::BdsB1cIntegrity(flags))
298                        },
299                        (NavMessageType::CNV2, Constellation::BeiDou) => {
300                            let flags = BdsB2aB1cIntegrity::from_bits(unsigned)
301                                .ok_or(ParsingError::NavFlagsMapping)?;
302
303                            Ok(OrbitItem::BdsB2aB1cIntegrity(flags))
304                        },
305                        (NavMessageType::CNV3, Constellation::BeiDou) => {
306                            let flags = BdsB2bIntegrity::from_bits(unsigned)
307                                .ok_or(ParsingError::NavFlagsMapping)?;
308
309                            Ok(OrbitItem::BdsB2bIntegrity(flags))
310                        },
311                        _ => Err(ParsingError::NavDataSourceDefinition),
312                    },
313                    "status" => {
314                        // complex status indication
315                        match (msgtype, constellation) {
316                            (NavMessageType::FDMA, Constellation::Glonass) => {
317                                let flags = GlonassStatus::from(unsigned);
318
319                                Ok(OrbitItem::GlonassStatus(flags))
320                            },
321                            _ => Err(ParsingError::NavHealthFlagDefinition),
322                        }
323                    },
324                    "l2p" => {
325                        // l2p flag from GPS navigation message
326                        Ok(OrbitItem::Gpsl2pFlag(unsigned > 0))
327                    },
328                    _ => Err(ParsingError::NavFlagsDefinition),
329                }
330            },
331            _ => {
332                // unknown complex type
333                Err(ParsingError::NavUnknownComplexType)
334            },
335        }
336    }
337
338    /// Unwraps [OrbitItem] as [f64] (always feasible)
339    pub fn as_f64(&self) -> f64 {
340        match self {
341            OrbitItem::F64(f) => *f,
342            OrbitItem::U8(val) => *val as f64,
343            OrbitItem::I8(val) => *val as f64,
344            OrbitItem::U32(val) => *val as f64,
345            OrbitItem::Gpsl2pFlag(flag) => (*flag as u8) as f64,
346            OrbitItem::GalHealth(flags) => flags.bits() as f64,
347            OrbitItem::GpsQzssl1cHealth(flags) => flags.bits() as f64,
348            OrbitItem::GpsQzssl1l2l5Health(flags) => flags.0 as f64,
349            OrbitItem::GeoHealth(flags) => flags.bits() as f64,
350            OrbitItem::IrnssHealth(flags) => flags.bits() as f64,
351            OrbitItem::GlonassHealth(flags) => flags.bits() as f64,
352            OrbitItem::GlonassStatus(status) => status.0 as f64,
353            OrbitItem::GalDataSource(source) => source.bits() as f64,
354            OrbitItem::GlonassHealth2(health) => health.bits() as f64,
355            OrbitItem::BdsSatH1(sat_h1) => sat_h1.bits() as f64,
356            OrbitItem::BdsHealth(health) => (*health as u32) as f64,
357            OrbitItem::BdsSatelliteType(sat) => (*sat as u32) as f64,
358            OrbitItem::BdsB1cIntegrity(integrity) => integrity.bits() as f64,
359            OrbitItem::BdsB2aB1cIntegrity(integrity) => integrity.bits() as f64,
360            OrbitItem::BdsB2bIntegrity(integrity) => integrity.bits() as f64,
361        }
362    }
363
364    /// Unwraps [OrbitItem] as [u32] (always feasible)
365    pub fn as_u32(&self) -> u32 {
366        match self {
367            OrbitItem::U32(v) => *v,
368            OrbitItem::U8(val) => *val as u32,
369            OrbitItem::I8(val) => *val as u32,
370            OrbitItem::F64(val) => val.round() as u32,
371            OrbitItem::Gpsl2pFlag(flag) => *flag as u32,
372            OrbitItem::GalHealth(health) => health.bits(),
373            OrbitItem::GpsQzssl1cHealth(flags) => flags.bits(),
374            OrbitItem::GpsQzssl1l2l5Health(flags) => flags.0,
375            OrbitItem::GeoHealth(health) => health.bits(),
376            OrbitItem::IrnssHealth(health) => health.bits(),
377            OrbitItem::GlonassHealth(health) => health.bits(),
378            OrbitItem::GlonassStatus(status) => status.0,
379            OrbitItem::GalDataSource(source) => source.bits(),
380            OrbitItem::GlonassHealth2(health) => health.bits(),
381            OrbitItem::BdsSatH1(sat_h1) => sat_h1.bits(),
382            OrbitItem::BdsHealth(health) => *health as u32,
383            OrbitItem::BdsSatelliteType(sat) => *sat as u32,
384            OrbitItem::BdsB1cIntegrity(integrity) => integrity.bits(),
385            OrbitItem::BdsB2aB1cIntegrity(integrity) => integrity.bits(),
386            OrbitItem::BdsB2bIntegrity(integrity) => integrity.bits(),
387        }
388    }
389
390    /// Unwraps [OrbitItem] as [u8] (always feasible)
391    pub fn as_u8(&self) -> u8 {
392        match self {
393            OrbitItem::U32(v) => *v as u8,
394            OrbitItem::U8(val) => *val,
395            OrbitItem::I8(val) => *val as u8,
396            OrbitItem::F64(val) => val.round() as u8,
397            OrbitItem::Gpsl2pFlag(flag) => *flag as u8,
398            OrbitItem::GalHealth(health) => health.bits() as u8,
399            OrbitItem::GpsQzssl1cHealth(flags) => flags.bits() as u8,
400            OrbitItem::GpsQzssl1l2l5Health(flags) => flags.0 as u8,
401            OrbitItem::GeoHealth(health) => health.bits() as u8,
402            OrbitItem::IrnssHealth(health) => health.bits() as u8,
403            OrbitItem::GlonassHealth(health) => health.bits() as u8,
404            OrbitItem::GlonassStatus(status) => status.0 as u8,
405            OrbitItem::GlonassHealth2(health) => health.bits() as u8,
406            OrbitItem::GalDataSource(source) => source.bits() as u8,
407            OrbitItem::BdsSatH1(sat_h1) => sat_h1.bits() as u8,
408            OrbitItem::BdsHealth(health) => (*health as u32) as u8,
409            OrbitItem::BdsSatelliteType(sat) => (*sat as u32) as u8,
410            OrbitItem::BdsB1cIntegrity(integrity) => integrity.bits() as u8,
411            OrbitItem::BdsB2aB1cIntegrity(integrity) => integrity.bits() as u8,
412            OrbitItem::BdsB2bIntegrity(integrity) => integrity.bits() as u8,
413        }
414    }
415
416    /// Unwraps [OrbitItem] as [i8] (always feasible)
417    pub fn as_i8(&self) -> i8 {
418        match self {
419            OrbitItem::U32(v) => *v as i8,
420            OrbitItem::I8(val) => *val,
421            OrbitItem::U8(val) => *val as i8,
422            OrbitItem::F64(val) => val.round() as i8,
423            OrbitItem::Gpsl2pFlag(flag) => *flag as i8,
424            OrbitItem::GalHealth(health) => health.bits() as i8,
425            OrbitItem::GpsQzssl1cHealth(flags) => flags.bits() as i8,
426            OrbitItem::GpsQzssl1l2l5Health(flags) => flags.0 as i8,
427            OrbitItem::GeoHealth(health) => health.bits() as i8,
428            OrbitItem::IrnssHealth(health) => health.bits() as i8,
429            OrbitItem::GlonassHealth(health) => health.bits() as i8,
430            OrbitItem::GlonassStatus(status) => status.0 as i8,
431            OrbitItem::GalDataSource(source) => source.bits() as i8,
432            OrbitItem::GlonassHealth2(health) => health.bits() as i8,
433            OrbitItem::BdsSatH1(sat_h1) => sat_h1.bits() as i8,
434            OrbitItem::BdsHealth(health) => (*health as u32) as i8,
435            OrbitItem::BdsSatelliteType(sat) => (*sat as u32) as i8,
436            OrbitItem::BdsB1cIntegrity(integrity) => integrity.bits() as i8,
437            OrbitItem::BdsB2aB1cIntegrity(integrity) => integrity.bits() as i8,
438            OrbitItem::BdsB2bIntegrity(integrity) => integrity.bits() as i8,
439        }
440    }
441
442    /// Unwraps Self as [Gpsl2pFlag] (if feasible)
443    pub fn as_gps_l2p_flag(&self) -> Option<bool> {
444        match self {
445            OrbitItem::Gpsl2pFlag(flag) => Some(*flag),
446            _ => None,
447        }
448    }
449
450    /// Unwraps Self as [GpsQzssl1l2l5Health] flag (if feasible).
451    pub fn as_gps_qzss_l1l2l5_health_flag(&self) -> Option<GpsQzssl1l2l5Health> {
452        match self {
453            OrbitItem::GpsQzssl1l2l5Health(h) => Some(h.clone()),
454            _ => None,
455        }
456    }
457
458    /// Unwraps Self as [GpsQzssl1cHealth] flag (if feasible).
459    pub fn as_gps_qzss_l1c_health_flag(&self) -> Option<GpsQzssl1cHealth> {
460        match self {
461            OrbitItem::GpsQzssl1cHealth(h) => Some(h.clone()),
462            _ => None,
463        }
464    }
465
466    /// Unwraps Self as [GeoHealth] flag (if feasible)
467    pub fn as_geo_health_flag(&self) -> Option<GeoHealth> {
468        match self {
469            OrbitItem::GeoHealth(h) => Some(h.clone()),
470            _ => None,
471        }
472    }
473
474    /// Unwraps Self as [GlonassHealth] flag (if feasible)
475    pub fn as_glonass_health_flag(&self) -> Option<GlonassHealth> {
476        match self {
477            OrbitItem::GlonassHealth(h) => Some(h.clone()),
478            _ => None,
479        }
480    }
481
482    /// Unwraps Self as [GlonassHealth2] flag (if feasible)
483    pub fn as_glonass_health2_flag(&self) -> Option<GlonassHealth2> {
484        match self {
485            OrbitItem::GlonassHealth2(h) => Some(h.clone()),
486            _ => None,
487        }
488    }
489
490    /// Unwraps Self as [GlonassStatus] mask (if feasible)
491    pub fn as_glonass_status_mask(&self) -> Option<GlonassStatus> {
492        match self {
493            OrbitItem::GlonassStatus(h) => Some(h.clone()),
494            _ => None,
495        }
496    }
497
498    /// Unwraps Self as [GalHealth] flag (if feasible)
499    pub fn as_galileo_health_flag(&self) -> Option<GalHealth> {
500        match self {
501            OrbitItem::GalHealth(h) => Some(*h),
502            _ => None,
503        }
504    }
505
506    /// Unwraps Self as historical (and D1/D2) [BdsSatH1] flag (if feasible)
507    pub fn as_bds_sat_h1_flag(&self) -> Option<BdsSatH1> {
508        match self {
509            OrbitItem::BdsSatH1(h) => Some(h.clone()),
510            _ => None,
511        }
512    }
513
514    /// Unwraps Self as modern [BdsHealth] flag (if feasible)
515    pub fn as_bds_health_flag(&self) -> Option<BdsHealth> {
516        match self {
517            OrbitItem::BdsHealth(h) => Some(h.clone()),
518            _ => None,
519        }
520    }
521
522    /// Unwraps Self as modern [BdsSatelliteType] indication (if feasible)
523    pub fn as_bds_satellite_type(&self) -> Option<BdsSatelliteType> {
524        match self {
525            OrbitItem::BdsSatelliteType(h) => Some(h.clone()),
526            _ => None,
527        }
528    }
529
530    /// Unwraps Self as [BdsB1cIntegrity] flag (if feasible)
531    pub fn as_bds_b1c_integrity(&self) -> Option<BdsB1cIntegrity> {
532        match self {
533            OrbitItem::BdsB1cIntegrity(h) => Some(h.clone()),
534            _ => None,
535        }
536    }
537
538    /// Unwraps Self as [BdsB2aB1cIntegrity] flag (if feasible)
539    pub fn as_bds_b2a_b1c_integrity(&self) -> Option<BdsB2aB1cIntegrity> {
540        match self {
541            OrbitItem::BdsB2aB1cIntegrity(h) => Some(h.clone()),
542            _ => None,
543        }
544    }
545
546    /// Unwraps Self as [BdsB2bIntegrity] flag (if feasible)
547    pub fn as_bds_b2b_integrity(&self) -> Option<BdsB2bIntegrity> {
548        match self {
549            OrbitItem::BdsB2bIntegrity(h) => Some(h.clone()),
550            _ => None,
551        }
552    }
553
554    /// Unwraps Self as [IrnssHealth] flag (if feasible)
555    pub fn as_irnss_health_flag(&self) -> Option<IrnssHealth> {
556        match self {
557            OrbitItem::IrnssHealth(h) => Some(h.clone()),
558            _ => None,
559        }
560    }
561}
562
563/// Identifies closest (but older) revision contained in NAV database.
564/// Closest content (in time) is used during record parsing to identify and sort data.
565/// Returns None
566/// - if no database entries were found for requested constellation.
567///  - or only newer revision exist : we prefer matching on older revisions
568pub(crate) fn closest_nav_standards(
569    constellation: Constellation,
570    revision: Version,
571    msg: NavMessageType,
572) -> Option<&'static NavHelper<'static>> {
573    let database = &NAV_ORBITS;
574    // start by trying to locate desired revision.
575    // On each mismatch, we decrement and move on to next major/minor combination.
576    let (mut major, mut minor): (u8, u8) = revision.into();
577
578    loop {
579        // Match constellation, message type & revision exactly
580        let items: Vec<_> = database
581            .iter()
582            .filter(|item| {
583                item.constellation == constellation
584                    && item.msg == msg
585                    && item.version == Version::new(major, minor)
586            })
587            .collect();
588
589        if items.is_empty() {
590            if minor == 0 {
591                // we're done with this major
592                // -> downgrade to previous major
593                //    we start @ minor = 10, which is
594                //    larger than most revisions we know
595                if major == 0 {
596                    // we're done
597                    break;
598                } else {
599                    major -= 1;
600                    minor = 10;
601                }
602            } else {
603                minor -= 1;
604            }
605        } else {
606            return Some(items[0]);
607        }
608    } // loop
609
610    None
611}
612
613#[cfg(test)]
614mod test {
615    use super::*;
616    use crate::navigation::NavMessageType;
617
618    #[test]
619    fn orbit_database_sanity() {
620        for frame in NAV_ORBITS.iter() {
621            let nav_msg = frame.msg;
622            let constellation = frame.constellation;
623
624            for (name_str, type_str) in frame.items.iter() {
625                let val_str = "1.2345";
626
627                let e = OrbitItem::new(name_str, type_str, val_str, &nav_msg, constellation);
628                assert!(
629                    e.is_ok(),
630                    "{}({}) {}:{} orbit item failed",
631                    constellation,
632                    nav_msg,
633                    name_str,
634                    type_str,
635                );
636            }
637        }
638    }
639
640    #[test]
641    fn nav_standards_finder() {
642        // Constellation::Mixed is not contained in db!
643        assert_eq!(
644            closest_nav_standards(
645                Constellation::Mixed,
646                Version::default(),
647                NavMessageType::LNAV
648            ),
649            None,
650            "Mixed GNSS constellation is or should not exist in the DB"
651        );
652
653        // Test existing (exact match) entries
654        for (constellation, rev, msg) in [
655            (Constellation::GPS, Version::new(1, 0), NavMessageType::LNAV),
656            (Constellation::GPS, Version::new(2, 0), NavMessageType::LNAV),
657            (Constellation::GPS, Version::new(4, 0), NavMessageType::LNAV),
658            (Constellation::GPS, Version::new(4, 0), NavMessageType::CNAV),
659            (Constellation::GPS, Version::new(4, 0), NavMessageType::CNV2),
660            (
661                Constellation::Glonass,
662                Version::new(2, 0),
663                NavMessageType::LNAV,
664            ),
665            (
666                Constellation::Glonass,
667                Version::new(3, 0),
668                NavMessageType::LNAV,
669            ),
670            (
671                Constellation::Galileo,
672                Version::new(3, 0),
673                NavMessageType::LNAV,
674            ),
675            (
676                Constellation::Galileo,
677                Version::new(4, 0),
678                NavMessageType::INAV,
679            ),
680            (
681                Constellation::Galileo,
682                Version::new(4, 0),
683                NavMessageType::FNAV,
684            ),
685            (
686                Constellation::QZSS,
687                Version::new(3, 0),
688                NavMessageType::LNAV,
689            ),
690            (
691                Constellation::QZSS,
692                Version::new(4, 0),
693                NavMessageType::LNAV,
694            ),
695            (
696                Constellation::QZSS,
697                Version::new(4, 0),
698                NavMessageType::CNAV,
699            ),
700            (
701                Constellation::QZSS,
702                Version::new(4, 0),
703                NavMessageType::CNV2,
704            ),
705            (
706                Constellation::BeiDou,
707                Version::new(3, 0),
708                NavMessageType::LNAV,
709            ),
710            // NAV V4 (exact)
711            (
712                Constellation::BeiDou,
713                Version::new(4, 0),
714                NavMessageType::D1,
715            ),
716            (
717                Constellation::BeiDou,
718                Version::new(4, 0),
719                NavMessageType::D2,
720            ),
721            (
722                Constellation::BeiDou,
723                Version::new(4, 0),
724                NavMessageType::CNV1,
725            ),
726            (
727                Constellation::BeiDou,
728                Version::new(4, 0),
729                NavMessageType::CNV2,
730            ),
731            (
732                Constellation::BeiDou,
733                Version::new(4, 0),
734                NavMessageType::CNV3,
735            ),
736            (
737                Constellation::SBAS,
738                Version::new(4, 0),
739                NavMessageType::SBAS,
740            ),
741        ] {
742            let found = closest_nav_standards(constellation, rev, msg);
743            assert!(
744                found.is_some(),
745                "should have identified {}:V{} ({}) frame that actually exists in DB",
746                constellation,
747                rev,
748                msg
749            );
750
751            let standards = found.unwrap();
752
753            assert!(
754                standards.constellation == constellation,
755                "bad constellation identified \"{}\", expecting \"{}\"",
756                constellation,
757                standards.constellation
758            );
759
760            assert!(
761                standards.version == rev,
762                "should have matched {} V{} exactly, because it exists in DB",
763                constellation,
764                rev,
765            );
766        }
767
768        // Test cases where the nearest revision is used, not that exact revision
769        for (constellation, desired, expected, msg) in [
770            (
771                Constellation::GPS,
772                Version::new(5, 0),
773                Version::new(4, 0),
774                NavMessageType::LNAV,
775            ),
776            (
777                Constellation::GPS,
778                Version::new(4, 1),
779                Version::new(4, 0),
780                NavMessageType::LNAV,
781            ),
782            (
783                Constellation::Glonass,
784                Version::new(3, 4),
785                Version::new(3, 0),
786                NavMessageType::LNAV,
787            ),
788            (
789                Constellation::BeiDou,
790                Version::new(4, 2),
791                Version::new(4, 0),
792                NavMessageType::CNV1,
793            ),
794            (
795                Constellation::BeiDou,
796                Version::new(4, 2),
797                Version::new(4, 0),
798                NavMessageType::CNV2,
799            ),
800            (
801                Constellation::BeiDou,
802                Version::new(4, 2),
803                Version::new(4, 0),
804                NavMessageType::CNV3,
805            ),
806            (
807                Constellation::Galileo,
808                Version::new(4, 2),
809                Version::new(4, 0),
810                NavMessageType::INAV,
811            ),
812            (
813                Constellation::QZSS,
814                Version::new(4, 2),
815                Version::new(4, 0),
816                NavMessageType::LNAV,
817            ),
818            (
819                Constellation::QZSS,
820                Version::new(4, 2),
821                Version::new(4, 0),
822                NavMessageType::CNAV,
823            ),
824        ] {
825            let found = closest_nav_standards(constellation, desired, msg);
826
827            assert!(
828                found.is_some(),
829                "should have converged for \"{}\" V\"{}\" (\"{}\") to nearest frame revision",
830                constellation,
831                desired,
832                msg
833            );
834
835            let standards = found.unwrap();
836
837            assert!(
838                standards.constellation == constellation,
839                "bad constellation identified \"{}\", expecting \"{}\"",
840                constellation,
841                standards.constellation
842            );
843
844            assert!(
845                standards.version == expected,
846                "closest_nav_standards() converged to wrong revision {}:{}({}) while \"{}\" was expected", 
847                constellation,
848                desired,
849                msg,
850                expected);
851        }
852    }
853
854    #[test]
855    fn test_db_item() {
856        let e = OrbitItem::U8(10);
857        assert_eq!(e.as_u8(), 10);
858
859        let e = OrbitItem::F64(10.0);
860        assert_eq!(e.as_u8(), 10);
861        assert_eq!(e.as_u32(), 10);
862        assert_eq!(e.as_f64(), 10.0);
863
864        let e = OrbitItem::U32(1);
865        assert_eq!(e.as_u32(), 1);
866        assert_eq!(e.as_f64(), 1.0);
867    }
868
869    #[test]
870    fn test_orbit_channel_5() {
871        let lnav = NavMessageType::LNAV;
872
873        let _ = OrbitItem::new(
874            "channel",
875            "i8",
876            "5.000000000000D+00",
877            &lnav,
878            Constellation::Glonass,
879        )
880        .unwrap();
881    }
882}