1mod store;
26pub use store::{BroadcastStore, NavMessagePreference};
27
28mod write;
29pub use write::encode_nav;
30
31use crate::astro::time::model::{GnssWeekTow, TimeScale};
32use crate::astro::time::{civil, gnss};
33use crate::broadcast::{ClockPolynomial, ConstellationConstants, KeplerianElements};
34use crate::constants::{KM_TO_M, SECONDS_PER_HOUR, SECONDS_PER_WEEK};
35use crate::format::columns::{field, raw_field};
36use crate::id::{GnssSatelliteId, GnssSystem};
37use crate::ionex::GalileoNequickCoeffs;
38use crate::validate::{self, FieldError};
39
40fn parse_f64(line: &str, start: usize, end: usize) -> Option<f64> {
45 let value = crate::format::columns::fortran_f64(line, start, end, "numeric field")?;
46 write::d19_12_representable(value).then_some(value)
53}
54
55pub(crate) const MAX_EPHEMERIS_AGE_S: f64 = 4.0 * SECONDS_PER_HOUR;
62
63pub(crate) const GLONASS_MAX_AGE_S: f64 = 15.0 * 60.0;
67const GPS_NOMINAL_FIT_INTERVAL_S: f64 = 4.0 * SECONDS_PER_HOUR;
68const GPS_LEGACY_EXTENDED_FIT_INTERVAL_S: f64 = 8.0 * SECONDS_PER_HOUR;
69const GLONASS_FREQ_CHANNEL_MIN: i32 = -7;
70const GLONASS_FREQ_CHANNEL_MAX: i32 = 6;
71
72pub(crate) fn valid_glonass_frequency_channel(channel: i32) -> bool {
73 (GLONASS_FREQ_CHANNEL_MIN..=GLONASS_FREQ_CHANNEL_MAX).contains(&channel)
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77struct RinexVersion {
78 major: u8,
79 minor: u8,
80}
81
82impl RinexVersion {
83 fn gps_fit_interval_uses_legacy_flag(self) -> bool {
84 self.major == 3 && self.minor <= 2
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum NavMessage {
91 GpsLnav,
93 GpsCnav,
95 GpsCnav2,
97 QzssCnav,
99 QzssCnav2,
101 GalileoInav,
103 GalileoFnav,
105 BeidouD1,
107 BeidouD2,
109}
110
111impl NavMessage {
112 pub const fn is_cnav_family(self) -> bool {
114 matches!(
115 self,
116 Self::GpsCnav | Self::GpsCnav2 | Self::QzssCnav | Self::QzssCnav2
117 )
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct BroadcastIssue {
124 pub issue: u32,
126 pub message: NavMessage,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum BroadcastGroupDelayTerm {
133 GpsTgd,
135 GalileoBgdE5aE1,
137 GalileoBgdE5bE1,
139 BeidouTgd1,
141 BeidouTgd2,
143 CnavIscL1Ca,
145 CnavIscL2C,
147 CnavIscL5I5,
149 CnavIscL5Q5,
151 CnavIscL1Cd,
153 CnavIscL1Cp,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum CnavSignal {
160 L1Ca,
162 L2C,
164 L5I5,
166 L5Q5,
168 L1Cp,
170 L1Cd,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Default)]
176pub struct BroadcastGroupDelays {
177 pub gps_tgd_s: Option<f64>,
179 pub galileo_bgd_e5a_e1_s: Option<f64>,
181 pub galileo_bgd_e5b_e1_s: Option<f64>,
183 pub beidou_tgd1_s: Option<f64>,
185 pub beidou_tgd2_s: Option<f64>,
187 pub cnav_isc_l1ca_s: Option<f64>,
189 pub cnav_isc_l2c_s: Option<f64>,
191 pub cnav_isc_l5i5_s: Option<f64>,
193 pub cnav_isc_l5q5_s: Option<f64>,
195 pub cnav_isc_l1cd_s: Option<f64>,
197 pub cnav_isc_l1cp_s: Option<f64>,
199}
200
201impl BroadcastGroupDelays {
202 pub const fn gps_lnav(tgd_s: f64) -> Self {
204 Self {
205 gps_tgd_s: Some(tgd_s),
206 galileo_bgd_e5a_e1_s: None,
207 galileo_bgd_e5b_e1_s: None,
208 beidou_tgd1_s: None,
209 beidou_tgd2_s: None,
210 cnav_isc_l1ca_s: None,
211 cnav_isc_l2c_s: None,
212 cnav_isc_l5i5_s: None,
213 cnav_isc_l5q5_s: None,
214 cnav_isc_l1cd_s: None,
215 cnav_isc_l1cp_s: None,
216 }
217 }
218
219 pub const fn galileo(bgd_e5a_e1_s: f64, bgd_e5b_e1_s: f64) -> Self {
221 Self {
222 gps_tgd_s: None,
223 galileo_bgd_e5a_e1_s: Some(bgd_e5a_e1_s),
224 galileo_bgd_e5b_e1_s: Some(bgd_e5b_e1_s),
225 beidou_tgd1_s: None,
226 beidou_tgd2_s: None,
227 cnav_isc_l1ca_s: None,
228 cnav_isc_l2c_s: None,
229 cnav_isc_l5i5_s: None,
230 cnav_isc_l5q5_s: None,
231 cnav_isc_l1cd_s: None,
232 cnav_isc_l1cp_s: None,
233 }
234 }
235
236 pub const fn beidou(tgd1_s: f64, tgd2_s: f64) -> Self {
238 Self {
239 gps_tgd_s: None,
240 galileo_bgd_e5a_e1_s: None,
241 galileo_bgd_e5b_e1_s: None,
242 beidou_tgd1_s: Some(tgd1_s),
243 beidou_tgd2_s: Some(tgd2_s),
244 cnav_isc_l1ca_s: None,
245 cnav_isc_l2c_s: None,
246 cnav_isc_l5i5_s: None,
247 cnav_isc_l5q5_s: None,
248 cnav_isc_l1cd_s: None,
249 cnav_isc_l1cp_s: None,
250 }
251 }
252
253 pub const fn cnav(
255 tgd_s: Option<f64>,
256 isc_l1ca_s: Option<f64>,
257 isc_l2c_s: Option<f64>,
258 isc_l5i5_s: Option<f64>,
259 isc_l5q5_s: Option<f64>,
260 isc_l1cd_s: Option<f64>,
261 isc_l1cp_s: Option<f64>,
262 ) -> Self {
263 Self {
264 gps_tgd_s: tgd_s,
265 galileo_bgd_e5a_e1_s: None,
266 galileo_bgd_e5b_e1_s: None,
267 beidou_tgd1_s: None,
268 beidou_tgd2_s: None,
269 cnav_isc_l1ca_s: isc_l1ca_s,
270 cnav_isc_l2c_s: isc_l2c_s,
271 cnav_isc_l5i5_s: isc_l5i5_s,
272 cnav_isc_l5q5_s: isc_l5q5_s,
273 cnav_isc_l1cd_s: isc_l1cd_s,
274 cnav_isc_l1cp_s: isc_l1cp_s,
275 }
276 }
277
278 pub const fn get(&self, term: BroadcastGroupDelayTerm) -> Option<f64> {
280 match term {
281 BroadcastGroupDelayTerm::GpsTgd => self.gps_tgd_s,
282 BroadcastGroupDelayTerm::GalileoBgdE5aE1 => self.galileo_bgd_e5a_e1_s,
283 BroadcastGroupDelayTerm::GalileoBgdE5bE1 => self.galileo_bgd_e5b_e1_s,
284 BroadcastGroupDelayTerm::BeidouTgd1 => self.beidou_tgd1_s,
285 BroadcastGroupDelayTerm::BeidouTgd2 => self.beidou_tgd2_s,
286 BroadcastGroupDelayTerm::CnavIscL1Ca => self.cnav_isc_l1ca_s,
287 BroadcastGroupDelayTerm::CnavIscL2C => self.cnav_isc_l2c_s,
288 BroadcastGroupDelayTerm::CnavIscL5I5 => self.cnav_isc_l5i5_s,
289 BroadcastGroupDelayTerm::CnavIscL5Q5 => self.cnav_isc_l5q5_s,
290 BroadcastGroupDelayTerm::CnavIscL1Cd => self.cnav_isc_l1cd_s,
291 BroadcastGroupDelayTerm::CnavIscL1Cp => self.cnav_isc_l1cp_s,
292 }
293 }
294
295 pub fn cnav_single_frequency_correction_s(&self, signal: CnavSignal) -> Option<f64> {
301 let isc = match signal {
302 CnavSignal::L1Ca => self.cnav_isc_l1ca_s,
303 CnavSignal::L2C => self.cnav_isc_l2c_s,
304 CnavSignal::L5I5 => self.cnav_isc_l5i5_s,
305 CnavSignal::L5Q5 => self.cnav_isc_l5q5_s,
306 CnavSignal::L1Cp => self.cnav_isc_l1cp_s,
307 CnavSignal::L1Cd => self.cnav_isc_l1cd_s,
308 }?;
309 Some(self.gps_tgd_s? - isc)
310 }
311
312 pub const fn for_message(self, system: GnssSystem, message: NavMessage) -> Option<f64> {
320 match (system, message) {
321 (GnssSystem::Gps, NavMessage::GpsLnav) => self.get(BroadcastGroupDelayTerm::GpsTgd),
322 (GnssSystem::Galileo, NavMessage::GalileoFnav) => {
323 self.get(BroadcastGroupDelayTerm::GalileoBgdE5aE1)
324 }
325 (GnssSystem::Galileo, NavMessage::GalileoInav) => {
326 self.get(BroadcastGroupDelayTerm::GalileoBgdE5bE1)
327 }
328 (GnssSystem::BeiDou, NavMessage::BeidouD1 | NavMessage::BeidouD2) => {
329 self.get(BroadcastGroupDelayTerm::BeidouTgd1)
330 }
331 (
332 GnssSystem::Gps | GnssSystem::Qzss,
333 NavMessage::GpsCnav
334 | NavMessage::GpsCnav2
335 | NavMessage::QzssCnav
336 | NavMessage::QzssCnav2,
337 ) => match (self.gps_tgd_s, self.cnav_isc_l1ca_s) {
338 (Some(tgd), Some(isc)) => Some(tgd - isc),
339 (Some(tgd), None) => Some(tgd),
340 (None, Some(isc)) => Some(-isc),
341 (None, None) => Some(0.0),
342 },
343 _ => None,
344 }
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq)]
350pub struct CnavParameters {
351 pub adot_m_s: f64,
353 pub delta_n0_dot_rad_s2: f64,
355 pub top: GnssWeekTow,
357 pub ura_ed_index: i8,
359 pub ura_ned0_index: i8,
361 pub ura_ned1_index: u8,
363 pub ura_ned2_index: u8,
365 pub transmission_time_sow: f64,
367 pub flags: Option<u32>,
369}
370
371pub fn cnav_ura_nominal_m(index: i8) -> Option<f64> {
375 match index {
376 -16 | 15 => None,
377 1 => Some(2.8),
378 3 => Some(5.7),
379 5 => Some(11.3),
380 -15..=6 => Some(2.0_f64.powf(1.0 + f64::from(index) / 2.0)),
381 7..=14 => Some(2.0_f64.powi(i32::from(index) - 2)),
382 _ => None,
383 }
384}
385
386pub fn cnav_ura_ned_m(params: &CnavParameters, t: GnssWeekTow) -> Option<f64> {
388 let ned0 = cnav_ura_nominal_m(params.ura_ned0_index)?;
389 let ned1 = 2.0_f64.powi(-(14 + i32::from(params.ura_ned1_index)));
390 let ned2 = 2.0_f64.powi(-(28 + i32::from(params.ura_ned2_index)));
391 let dt_op = (f64::from(t.week) - f64::from(params.top.week)) * SECONDS_PER_WEEK
392 + (t.tow_s - params.top.tow_s);
393 let linear = ned0 + ned1 * dt_op;
394 if dt_op <= 93_600.0 {
395 Some(linear)
396 } else {
397 Some(linear + ned2 * (dt_op - 93_600.0) * (dt_op - 93_600.0))
398 }
399}
400
401pub fn is_beidou_geo(sat: GnssSatelliteId) -> bool {
404 sat.system == GnssSystem::BeiDou && (sat.prn <= 5 || (59..=61).contains(&sat.prn))
405}
406
407#[derive(Debug, Clone, Copy, PartialEq)]
411pub struct KlobucharAlphaBeta {
412 pub alpha: [f64; 4],
414 pub beta: [f64; 4],
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Default)]
425pub struct IonoCorrections {
426 pub gps: Option<KlobucharAlphaBeta>,
428 pub beidou: Option<KlobucharAlphaBeta>,
430 pub galileo: Option<GalileoNequickCoeffs>,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq)]
438pub struct GlonassRecord {
439 pub satellite_id: GnssSatelliteId,
441 pub toe_utc_j2000_s: f64,
444 pub pos_m: [f64; 3],
446 pub vel_m_s: [f64; 3],
448 pub acc_m_s2: [f64; 3],
450 pub clk_bias: f64,
452 pub gamma_n: f64,
454 pub sv_health: f64,
456 pub freq_channel: i32,
458}
459
460#[derive(Debug, Clone, PartialEq, Eq)]
464pub struct SkippedGlonass {
465 pub token: String,
467}
468
469#[derive(Debug, Clone, PartialEq, Default)]
477pub struct GlonassParse {
478 pub records: Vec<GlonassRecord>,
480 pub skipped: Vec<SkippedGlonass>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq)]
486pub struct BroadcastRecord {
487 pub satellite_id: GnssSatelliteId,
489 pub message: NavMessage,
491 pub issue_of_data: BroadcastIssue,
493 pub week: u32,
495 pub toe: GnssWeekTow,
497 pub toc: GnssWeekTow,
499 pub elements: KeplerianElements,
501 pub clock: ClockPolynomial,
503 pub group_delays: BroadcastGroupDelays,
505 pub cnav: Option<CnavParameters>,
507 pub sv_health: f64,
509 pub sv_accuracy_m: f64,
511 pub fit_interval_s: Option<f64>,
516}
517
518impl BroadcastRecord {
519 pub const fn time_scale(&self) -> TimeScale {
521 self.toe.system
522 }
523
524 pub const fn constants(&self) -> ConstellationConstants {
526 match self.satellite_id.system {
527 GnssSystem::Galileo => ConstellationConstants::GALILEO,
528 GnssSystem::BeiDou => ConstellationConstants::BEIDOU,
529 _ => ConstellationConstants::GPS,
531 }
532 }
533
534 pub fn broadcast_clock_group_delay_s(&self) -> f64 {
536 self.group_delays
537 .for_message(self.satellite_id.system, self.message)
538 .unwrap_or(0.0)
539 }
540
541 pub fn from_lnav(
575 decoded: &crate::navigation::lnav::LnavDecoded,
576 satellite_id: GnssSatelliteId,
577 full_week: u32,
578 ) -> Result<Self, LnavRecordError> {
579 if satellite_id.system != GnssSystem::Gps {
580 return Err(LnavRecordError::NotGps(satellite_id));
581 }
582
583 if i64::from(full_week % 1024) != decoded.week_number {
588 return Err(LnavRecordError::WeekMismatch {
589 full_week,
590 decoded_week: decoded.week_number,
591 });
592 }
593
594 let sv_accuracy_m = gps_ura_index_to_meters(decoded.ura_index)
595 .ok_or(LnavRecordError::NoUraPrediction(decoded.ura_index))?;
596 let fit_interval_s =
597 gps_fit_interval_from_flag(decoded.fit_interval_flag, decoded.iode, decoded.iodc)?;
598
599 const SEMICIRCLE_TO_RAD: f64 = core::f64::consts::PI;
602
603 let elements = KeplerianElements {
604 sqrt_a: decoded.sqrt_a,
605 e: decoded.eccentricity,
606 m0: decoded.m0 * SEMICIRCLE_TO_RAD,
607 delta_n: decoded.delta_n * SEMICIRCLE_TO_RAD,
608 omega0: decoded.omega0 * SEMICIRCLE_TO_RAD,
609 i0: decoded.i0 * SEMICIRCLE_TO_RAD,
610 omega: decoded.omega * SEMICIRCLE_TO_RAD,
611 omega_dot: decoded.omega_dot * SEMICIRCLE_TO_RAD,
612 idot: decoded.idot * SEMICIRCLE_TO_RAD,
613 cuc: decoded.cuc,
614 cus: decoded.cus,
615 crc: decoded.crc,
616 crs: decoded.crs,
617 cic: decoded.cic,
618 cis: decoded.cis,
619 toe_sow: decoded.toe as f64,
620 };
621 let clock = ClockPolynomial {
622 af0: decoded.af0,
623 af1: decoded.af1,
624 af2: decoded.af2,
625 toc_sow: decoded.toc as f64,
626 };
627
628 let toe = GnssWeekTow::new(TimeScale::Gpst, full_week, elements.toe_sow)
629 .and_then(GnssWeekTow::normalized)
630 .map_err(|_| LnavRecordError::InvalidEpoch("toe"))?;
631 let toc = GnssWeekTow::new(TimeScale::Gpst, full_week, clock.toc_sow)
632 .and_then(GnssWeekTow::normalized)
633 .map_err(|_| LnavRecordError::InvalidEpoch("toc"))?;
634
635 Ok(BroadcastRecord {
636 satellite_id,
637 message: NavMessage::GpsLnav,
638 issue_of_data: BroadcastIssue {
639 issue: decoded.iode as u32,
640 message: NavMessage::GpsLnav,
641 },
642 week: full_week,
643 toe,
644 toc,
645 elements,
646 clock,
647 group_delays: BroadcastGroupDelays::gps_lnav(decoded.tgd),
648 cnav: None,
649 sv_health: decoded.sv_health as f64,
650 sv_accuracy_m,
651 fit_interval_s: Some(fit_interval_s),
652 })
653 }
654}
655
656fn gps_ura_index_to_meters(index: i64) -> Option<f64> {
662 let meters = match index {
663 0 => 2.4,
664 1 => 3.4,
665 2 => 4.85,
666 3 => 6.85,
667 4 => 9.65,
668 5 => 13.65,
669 6 => 24.0,
670 7 => 48.0,
671 8 => 96.0,
672 9 => 192.0,
673 10 => 384.0,
674 11 => 768.0,
675 12 => 1536.0,
676 13 => 3072.0,
677 14 => 6144.0,
678 _ => return None,
681 };
682 Some(meters)
683}
684
685const GPS_FIT_INTERVAL_6H_S: f64 = 6.0 * SECONDS_PER_HOUR;
686const GPS_FIT_INTERVAL_8H_S: f64 = 8.0 * SECONDS_PER_HOUR;
687const GPS_FIT_INTERVAL_14H_S: f64 = 14.0 * SECONDS_PER_HOUR;
688const GPS_FIT_INTERVAL_26H_S: f64 = 26.0 * SECONDS_PER_HOUR;
689
690fn gps_fit_interval_from_flag(
700 fit_interval_flag: i64,
701 iode: i64,
702 iodc: i64,
703) -> Result<f64, LnavRecordError> {
704 let unsupported = || LnavRecordError::FitIntervalUnsupported {
705 fit_interval_flag,
706 iode,
707 iodc,
708 };
709 match fit_interval_flag {
710 0 => Ok(GPS_NOMINAL_FIT_INTERVAL_S),
711 1 => {
712 if (0..240).contains(&iode) {
713 Ok(GPS_FIT_INTERVAL_6H_S)
717 } else if (240..=255).contains(&iode) {
718 match iodc {
720 240..=247 => Ok(GPS_FIT_INTERVAL_8H_S),
721 248..=255 | 496 => Ok(GPS_FIT_INTERVAL_14H_S),
722 497..=503 | 1021..=1023 => Ok(GPS_FIT_INTERVAL_26H_S),
723 _ => Err(unsupported()),
724 }
725 } else {
726 Err(unsupported())
727 }
728 }
729 _ => Err(unsupported()),
730 }
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum LnavRecordError {
736 NotGps(GnssSatelliteId),
738 InvalidEpoch(&'static str),
740 WeekMismatch {
743 full_week: u32,
745 decoded_week: i64,
747 },
748 NoUraPrediction(i64),
750 FitIntervalUnsupported {
753 fit_interval_flag: i64,
755 iode: i64,
757 iodc: i64,
759 },
760}
761
762impl core::fmt::Display for LnavRecordError {
763 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
764 match self {
765 LnavRecordError::NotGps(sat) => {
766 write!(f, "LNAV is a GPS message; {sat} is not a GPS satellite")
767 }
768 LnavRecordError::InvalidEpoch(field) => {
769 write!(f, "derived {field} week/TOW is not representable")
770 }
771 LnavRecordError::WeekMismatch {
772 full_week,
773 decoded_week,
774 } => write!(
775 f,
776 "full_week {full_week} (week % 1024 = {}) disagrees with decoded 10-bit week {decoded_week}",
777 full_week % 1024
778 ),
779 LnavRecordError::NoUraPrediction(index) => {
780 write!(f, "URA index {index} carries no accuracy prediction")
781 }
782 LnavRecordError::FitIntervalUnsupported {
783 fit_interval_flag,
784 iode,
785 iodc,
786 } => write!(
787 f,
788 "fit interval flag {fit_interval_flag} with IODE {iode} / IODC {iodc} is not a defined curve-fit interval"
789 ),
790 }
791 }
792}
793
794impl std::error::Error for LnavRecordError {}
795
796fn broadcast_time_scale(system: GnssSystem) -> TimeScale {
797 match system {
798 GnssSystem::Galileo => TimeScale::Gst,
799 GnssSystem::BeiDou => TimeScale::Bdt,
800 _ => TimeScale::Gpst,
801 }
802}
803
804#[derive(Debug, Clone, PartialEq, Eq)]
806pub enum NavParseError {
807 UnsupportedHeader(String),
809 MissingHeaderEnd,
811 TruncatedRecord(String),
813 BadField {
815 satellite: String,
817 field: &'static str,
819 },
820 BadHeaderField {
822 field: &'static str,
824 },
825}
826
827impl core::fmt::Display for NavParseError {
828 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
829 match self {
830 NavParseError::UnsupportedHeader(s) => write!(f, "unsupported RINEX NAV header: {s}"),
831 NavParseError::MissingHeaderEnd => write!(f, "no END OF HEADER line"),
832 NavParseError::TruncatedRecord(s) => write!(f, "truncated navigation record for {s}"),
833 NavParseError::BadField { satellite, field } => {
834 write!(f, "bad/missing {field} field in record for {satellite}")
835 }
836 NavParseError::BadHeaderField { field } => {
837 write!(f, "bad/missing {field} field in navigation header")
838 }
839 }
840 }
841}
842
843impl std::error::Error for NavParseError {}
844
845#[derive(Debug, Clone, PartialEq, Eq)]
846pub struct SkippedNavBlock {
847 pub satellite: String,
848 pub message: String,
849}
850
851#[derive(Debug, Clone, PartialEq)]
852pub struct NavParse {
853 pub records: Vec<BroadcastRecord>,
854 pub skipped: Vec<SkippedNavBlock>,
855}
856
857pub fn parse_nav(text: &str) -> Result<Vec<BroadcastRecord>, NavParseError> {
865 let mut lines = text.lines();
866 let version = verify_and_skip_header(&mut lines)?;
867 if version.major >= 4 {
868 parse_nav_v4(lines, version)
869 } else {
870 parse_nav_v3(lines, version)
871 }
872}
873
874pub fn parse_nav_lenient(text: &str) -> Result<NavParse, NavParseError> {
880 let mut lines = text.lines();
881 let version = verify_and_skip_header(&mut lines)?;
882 let (records, skipped) = if version.major >= 4 {
883 parse_nav_v4_lenient(lines, version)
884 } else {
885 parse_nav_v3_lenient(lines, version)
886 };
887 Ok(NavParse { records, skipped })
888}
889
890fn parse_nav_v3<'a, I>(
893 lines: I,
894 version: RinexVersion,
895) -> Result<Vec<BroadcastRecord>, NavParseError>
896where
897 I: Iterator<Item = &'a str>,
898{
899 let mut blocks: Vec<Vec<&str>> = Vec::new();
900 for line in lines {
901 if is_record_start(line) {
902 blocks.push(vec![line]);
903 } else if let Some(last) = blocks.last_mut() {
904 last.push(line);
905 }
906 }
907
908 let mut records = Vec::new();
909 for block in &blocks {
910 let letter = block[0].as_bytes()[0] as char;
911 match GnssSystem::from_letter(letter) {
912 Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou) => {
913 records.push(parse_keplerian_block(block, None, version)?);
914 }
915 _ => {}
917 }
918 }
919 Ok(records)
920}
921
922fn parse_nav_v3_lenient<'a, I>(
923 lines: I,
924 version: RinexVersion,
925) -> (Vec<BroadcastRecord>, Vec<SkippedNavBlock>)
926where
927 I: Iterator<Item = &'a str>,
928{
929 let mut blocks: Vec<Vec<&str>> = Vec::new();
930 for line in lines {
931 if is_record_start(line) {
932 blocks.push(vec![line]);
933 } else if let Some(last) = blocks.last_mut() {
934 last.push(line);
935 }
936 }
937
938 let mut records = Vec::new();
939 let mut skipped = Vec::new();
940 for block in &blocks {
941 let letter = block[0].as_bytes()[0] as char;
942 match GnssSystem::from_letter(letter) {
943 Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou) => {
944 match parse_keplerian_block(block, None, version) {
945 Ok(record) => records.push(record),
946 Err(error) => skipped.push(SkippedNavBlock {
947 satellite: nav_block_satellite(block),
948 message: error.to_string(),
949 }),
950 }
951 }
952 _ => {}
953 }
954 }
955 (records, skipped)
956}
957
958fn parse_nav_v4<'a, I>(
966 lines: I,
967 version: RinexVersion,
968) -> Result<Vec<BroadcastRecord>, NavParseError>
969where
970 I: Iterator<Item = &'a str>,
971{
972 let frames = v4_frames(lines);
975 let mut records = Vec::new();
976 for (marker, body) in &frames {
977 let Some((frame_type, sv, msg_token)) = parse_v4_marker(marker) else {
978 continue;
979 };
980 if frame_type != "EPH" {
981 continue; }
983 let letter = sv.as_bytes().first().copied().map_or(' ', char::from);
984 let Some(system) = GnssSystem::from_letter(letter) else {
985 continue;
986 };
987 let supported = matches!(
988 system,
989 GnssSystem::Gps | GnssSystem::Galileo | GnssSystem::BeiDou | GnssSystem::Qzss
990 );
991 if !supported {
992 continue; }
994 if let Some(message) = nav_message_from_v4_token(msg_token, system) {
995 validate_v4_ephemeris_marker(sv, message, body)?;
996 if message.is_cnav_family() {
997 records.push(parse_cnav_block(body, message)?);
998 } else {
999 records.push(parse_keplerian_block(body, Some(message), version)?);
1000 }
1001 } else if known_v4_ephemeris_token(msg_token)
1002 && !explicitly_skipped_v4_message(msg_token, system)
1003 {
1004 return Err(NavParseError::BadField {
1005 satellite: sv.to_string(),
1006 field: "message",
1007 });
1008 }
1009 }
1010 Ok(records)
1011}
1012
1013fn parse_nav_v4_lenient<'a, I>(
1014 lines: I,
1015 version: RinexVersion,
1016) -> (Vec<BroadcastRecord>, Vec<SkippedNavBlock>)
1017where
1018 I: Iterator<Item = &'a str>,
1019{
1020 let frames = v4_frames(lines);
1021 let mut records = Vec::new();
1022 let mut skipped = Vec::new();
1023 for (marker, body) in &frames {
1024 let Some((frame_type, sv, msg_token)) = parse_v4_marker(marker) else {
1025 continue;
1026 };
1027 if frame_type != "EPH" {
1028 continue;
1029 }
1030 let letter = sv.as_bytes().first().copied().map_or(' ', char::from);
1031 let Some(system) = GnssSystem::from_letter(letter) else {
1032 continue;
1033 };
1034 let supported = matches!(
1035 system,
1036 GnssSystem::Gps | GnssSystem::Qzss | GnssSystem::Galileo | GnssSystem::BeiDou
1037 );
1038 if !supported {
1039 continue;
1040 }
1041 if let Some(message) = nav_message_from_v4_token(msg_token, system) {
1042 let parsed = validate_v4_ephemeris_marker(sv, message, body)
1043 .and_then(|()| parse_keplerian_block(body, Some(message), version));
1044 match parsed {
1045 Ok(record) => records.push(record),
1046 Err(error) => skipped.push(SkippedNavBlock {
1047 satellite: sv.to_string(),
1048 message: error.to_string(),
1049 }),
1050 }
1051 }
1052 }
1053 (records, skipped)
1054}
1055
1056fn nav_block_satellite(block: &[&str]) -> String {
1057 block
1058 .first()
1059 .and_then(|line| line.get(0..3))
1060 .unwrap_or("")
1061 .trim()
1062 .to_string()
1063}
1064
1065fn v4_frames<'a, I>(lines: I) -> Vec<(&'a str, Vec<&'a str>)>
1066where
1067 I: Iterator<Item = &'a str>,
1068{
1069 let mut frames: Vec<(&str, Vec<&str>)> = Vec::new();
1070 for line in lines {
1071 if is_v4_frame_marker(line) {
1072 frames.push((line, Vec::new()));
1073 } else if let Some((_, body)) = frames.last_mut() {
1074 body.push(line);
1075 }
1076 }
1077 frames
1078}
1079
1080fn is_v4_frame_marker(line: &str) -> bool {
1082 line.starts_with("> ")
1083}
1084
1085fn parse_v4_marker(line: &str) -> Option<(&str, &str, &str)> {
1089 let rest = line.strip_prefix('>')?;
1090 let mut fields = rest.split_whitespace();
1091 let frame_type = fields.next()?;
1092 let sv = fields.next()?;
1093 let msg_token = fields.next()?;
1094 Some((frame_type, sv, msg_token))
1095}
1096
1097fn nav_message_from_v4_token(token: &str, system: GnssSystem) -> Option<NavMessage> {
1101 match (token, system) {
1102 ("LNAV", GnssSystem::Gps) => Some(NavMessage::GpsLnav),
1103 ("CNAV", GnssSystem::Gps) => Some(NavMessage::GpsCnav),
1104 ("CNV2", GnssSystem::Gps) => Some(NavMessage::GpsCnav2),
1105 ("CNAV", GnssSystem::Qzss) => Some(NavMessage::QzssCnav),
1106 ("CNV2", GnssSystem::Qzss) => Some(NavMessage::QzssCnav2),
1107 ("INAV", GnssSystem::Galileo) => Some(NavMessage::GalileoInav),
1108 ("FNAV", GnssSystem::Galileo) => Some(NavMessage::GalileoFnav),
1109 ("D1", GnssSystem::BeiDou) => Some(NavMessage::BeidouD1),
1110 ("D2", GnssSystem::BeiDou) => Some(NavMessage::BeidouD2),
1111 _ => None,
1112 }
1113}
1114
1115fn known_v4_ephemeris_token(token: &str) -> bool {
1116 matches!(
1117 token,
1118 "LNAV" | "CNAV" | "CNV1" | "CNV2" | "CNV3" | "INAV" | "FNAV" | "D1" | "D2"
1119 )
1120}
1121
1122fn explicitly_skipped_v4_message(token: &str, system: GnssSystem) -> bool {
1123 matches!(
1124 (token, system),
1125 ("LNAV", GnssSystem::Qzss) | ("CNV1" | "CNV2" | "CNV3", GnssSystem::BeiDou)
1126 )
1127}
1128
1129fn validate_v4_ephemeris_marker(
1130 marker_sv: &str,
1131 message: NavMessage,
1132 body: &[&str],
1133) -> Result<(), NavParseError> {
1134 let Some(body_sv) = body
1135 .first()
1136 .and_then(|line| line.get(0..3))
1137 .map(str::trim)
1138 .filter(|sv| !sv.is_empty())
1139 else {
1140 return Ok(());
1141 };
1142
1143 let same_satellite = match (
1144 marker_sv.parse::<GnssSatelliteId>(),
1145 body_sv.parse::<GnssSatelliteId>(),
1146 ) {
1147 (Ok(marker), Ok(body)) => marker == body,
1148 _ => marker_sv == body_sv,
1149 };
1150
1151 if !same_satellite {
1152 return Err(NavParseError::BadField {
1153 satellite: marker_sv.to_string(),
1154 field: "frame marker",
1155 });
1156 }
1157
1158 let system = body_sv
1159 .as_bytes()
1160 .first()
1161 .and_then(|b| GnssSystem::from_letter(*b as char))
1162 .ok_or_else(|| NavParseError::BadField {
1163 satellite: body_sv.to_string(),
1164 field: "system",
1165 })?;
1166 if !nav_message_matches_system(message, system) {
1167 return Err(NavParseError::BadField {
1168 satellite: body_sv.to_string(),
1169 field: "message",
1170 });
1171 }
1172
1173 Ok(())
1174}
1175
1176fn nav_message_matches_system(message: NavMessage, system: GnssSystem) -> bool {
1177 matches!(
1178 (message, system),
1179 (NavMessage::GpsLnav, GnssSystem::Gps)
1180 | (NavMessage::GpsCnav | NavMessage::GpsCnav2, GnssSystem::Gps)
1181 | (
1182 NavMessage::QzssCnav | NavMessage::QzssCnav2,
1183 GnssSystem::Qzss,
1184 )
1185 | (
1186 NavMessage::GalileoInav | NavMessage::GalileoFnav,
1187 GnssSystem::Galileo,
1188 )
1189 | (
1190 NavMessage::BeidouD1 | NavMessage::BeidouD2,
1191 GnssSystem::BeiDou,
1192 )
1193 )
1194}
1195
1196pub fn parse_iono_corrections(text: &str) -> Result<IonoCorrections, NavParseError> {
1204 parse_iono_corrections_checked(text)
1205}
1206
1207fn parse_iono_corrections_checked(text: &str) -> Result<IonoCorrections, NavParseError> {
1208 let klobuchar_row = |line: &str| -> Result<[f64; 4], NavParseError> {
1215 Ok([
1216 strict_header_f64(line, 5, 17, "ionospheric correction")?,
1217 strict_header_f64(line, 17, 29, "ionospheric correction")?,
1218 strict_header_f64(line, 29, 41, "ionospheric correction")?,
1219 strict_header_f64(line, 41, 53, "ionospheric correction")?,
1220 ])
1221 };
1222 let nequick_row = |line: &str| -> Result<[f64; 3], NavParseError> {
1227 Ok([
1228 strict_header_f64(line, 5, 17, "ionospheric correction")?,
1229 strict_header_f64(line, 17, 29, "ionospheric correction")?,
1230 strict_header_f64(line, 29, 41, "ionospheric correction")?,
1231 ])
1232 };
1233 let (mut gpsa, mut gpsb, mut bdsa, mut bdsb, mut gal) = (None, None, None, None, None);
1234 for line in text.lines() {
1235 if line.contains("END OF HEADER") {
1236 break;
1237 }
1238 if !line.contains("IONOSPHERIC CORR") {
1239 continue;
1240 }
1241 match line.get(0..4).map(str::trim) {
1242 Some("GPSA") => gpsa = Some(klobuchar_row(line)?),
1243 Some("GPSB") => gpsb = Some(klobuchar_row(line)?),
1244 Some("BDSA") => bdsa = Some(klobuchar_row(line)?),
1245 Some("BDSB") => bdsb = Some(klobuchar_row(line)?),
1246 Some("GAL") => {
1247 let row = nequick_row(line)?;
1248 gal = Some(GalileoNequickCoeffs {
1249 ai0: row[0],
1250 ai1: row[1],
1251 ai2: row[2],
1252 });
1253 }
1254 _ => {}
1255 }
1256 }
1257 let pair = |a: Option<[f64; 4]>, b: Option<[f64; 4]>| match (a, b) {
1258 (Some(alpha), Some(beta)) => Some(KlobucharAlphaBeta { alpha, beta }),
1259 _ => None,
1260 };
1261 let mut iono = IonoCorrections {
1262 gps: pair(gpsa, gpsb),
1263 beidou: pair(bdsa, bdsb),
1264 galileo: gal,
1265 };
1266 parse_v4_body_iono_corrections(text, &mut iono)?;
1267 Ok(iono)
1268}
1269
1270fn parse_v4_body_iono_corrections(
1271 text: &str,
1272 iono: &mut IonoCorrections,
1273) -> Result<(), NavParseError> {
1274 let mut lines = text.lines();
1275 for line in lines.by_ref() {
1276 if line.contains("END OF HEADER") {
1277 break;
1278 }
1279 }
1280
1281 for (marker, body) in v4_frames(lines) {
1282 let Some((frame_type, sv, _msg_token)) = parse_v4_marker(marker) else {
1283 continue;
1284 };
1285 if frame_type != "ION" {
1286 continue;
1287 }
1288 let values = parse_v4_iono_values(sv, &body)?;
1289 match sv
1290 .as_bytes()
1291 .first()
1292 .and_then(|b| GnssSystem::from_letter(*b as char))
1293 {
1294 Some(GnssSystem::Gps) => {
1295 iono.gps = Some(KlobucharAlphaBeta {
1296 alpha: iono_values_4(&values, 0, sv)?,
1297 beta: iono_values_4(&values, 4, sv)?,
1298 });
1299 }
1300 Some(GnssSystem::BeiDou) => {
1301 iono.beidou = Some(KlobucharAlphaBeta {
1302 alpha: iono_values_4(&values, 0, sv)?,
1303 beta: iono_values_4(&values, 4, sv)?,
1304 });
1305 }
1306 Some(GnssSystem::Galileo) => {
1307 let coeffs = iono_values_3(&values, 0, sv)?;
1308 iono.galileo = Some(GalileoNequickCoeffs {
1309 ai0: coeffs[0],
1310 ai1: coeffs[1],
1311 ai2: coeffs[2],
1312 });
1313 }
1314 _ => {}
1315 }
1316 }
1317 Ok(())
1318}
1319
1320fn parse_v4_iono_values(sv: &str, body: &[&str]) -> Result<Vec<f64>, NavParseError> {
1321 if body.is_empty() {
1322 return Err(NavParseError::BadField {
1323 satellite: sv.to_string(),
1324 field: "ionospheric correction",
1325 });
1326 }
1327
1328 let mut values = Vec::new();
1329 for (idx, line) in body.iter().enumerate() {
1330 let ranges: &[(usize, usize)] = if idx == 0 {
1331 &[(23, 42), (42, 61), (61, 80)]
1332 } else {
1333 &[(4, 23), (23, 42), (42, 61), (61, 80)]
1334 };
1335 for &(start, end) in ranges {
1336 let raw = raw_field(line, start, end);
1337 if raw.trim().is_empty() {
1338 continue;
1339 }
1340 values.push(
1341 validate::strict_f64(raw, "ionospheric correction")
1342 .map_err(|error| map_record_field_error(error, sv))?,
1343 );
1344 }
1345 }
1346 Ok(values)
1347}
1348
1349fn iono_values_4(values: &[f64], start: usize, sv: &str) -> Result<[f64; 4], NavParseError> {
1350 let Some(slice) = values.get(start..start + 4) else {
1351 return Err(NavParseError::BadField {
1352 satellite: sv.to_string(),
1353 field: "ionospheric correction",
1354 });
1355 };
1356 Ok([slice[0], slice[1], slice[2], slice[3]])
1357}
1358
1359fn iono_values_3(values: &[f64], start: usize, sv: &str) -> Result<[f64; 3], NavParseError> {
1360 let Some(slice) = values.get(start..start + 3) else {
1361 return Err(NavParseError::BadField {
1362 satellite: sv.to_string(),
1363 field: "ionospheric correction",
1364 });
1365 };
1366 Ok([slice[0], slice[1], slice[2]])
1367}
1368
1369pub fn parse_leap_seconds(text: &str) -> Result<Option<f64>, NavParseError> {
1373 parse_leap_seconds_checked(text)
1374}
1375
1376fn parse_leap_seconds_checked(text: &str) -> Result<Option<f64>, NavParseError> {
1377 for line in text.lines() {
1378 if line.contains("END OF HEADER") {
1379 break;
1380 }
1381 if line.contains("LEAP SECONDS") {
1382 return strict_header_integer_f64(line, 0, 6, "leap seconds").map(Some);
1383 }
1384 }
1385 Ok(None)
1386}
1387
1388fn j2000_seconds_utc(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> f64 {
1393 civil::j2000_seconds(y as i32, mo as i32, d as i32, h as i32, mi as i32, s as f64)
1394}
1395
1396fn parse_glonass_epoch(l0: &str, sat: &str) -> Result<f64, NavParseError> {
1399 let year = strict_record_int::<i64>(l0, 4, 8, "epoch", sat)?;
1400 let month = strict_record_int::<i64>(l0, 9, 11, "epoch", sat)?;
1401 let day = strict_record_int::<i64>(l0, 12, 14, "epoch", sat)?;
1402 let hour = strict_record_int::<i64>(l0, 15, 17, "epoch", sat)?;
1403 let minute = strict_record_int::<i64>(l0, 18, 20, "epoch", sat)?;
1404 let second = strict_record_int::<i64>(l0, 21, 23, "epoch", sat)?;
1405 let civil = validate::civil_datetime_with_second_policy(
1406 year,
1407 month,
1408 day,
1409 hour,
1410 minute,
1411 second as f64,
1412 validate::CivilSecondPolicy::UtcLike,
1413 )
1414 .map_err(|_| NavParseError::BadField {
1415 satellite: sat.to_string(),
1416 field: "epoch",
1417 })?;
1418 Ok(j2000_seconds_utc(
1419 civil.year,
1420 i64::from(civil.month),
1421 i64::from(civil.day),
1422 i64::from(civil.hour),
1423 i64::from(civil.minute),
1424 civil.second as i64,
1425 ))
1426}
1427
1428fn parse_glonass_block(block: &[&str]) -> Result<GlonassRecord, NavParseError> {
1432 let l0 = block[0];
1433 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1434 if block.len() < 4 {
1435 return Err(NavParseError::TruncatedRecord(sat));
1436 }
1437 let bad = |what: &'static str| NavParseError::BadField {
1438 satellite: sat.clone(),
1439 field: what,
1440 };
1441 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1442 let toe_utc_j2000_s = parse_glonass_epoch(l0, &sat)?;
1443 let clk_bias = parse_f64(l0, 23, 42).ok_or_else(|| bad("clock bias"))?;
1444 let gamma_n = parse_f64(l0, 42, 61).ok_or_else(|| bad("gamma_n"))?;
1445 let o1 = orbit_row(block[1]);
1446 let o2 = orbit_row(block[2]);
1447 let o3 = orbit_row(block[3]);
1448 let km = |v: Option<f64>, what: &'static str| v.map(|x| x * KM_TO_M).ok_or_else(|| bad(what));
1449 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1450 Ok(GlonassRecord {
1451 satellite_id,
1452 toe_utc_j2000_s,
1453 pos_m: [km(o1[0], "x")?, km(o2[0], "y")?, km(o3[0], "z")?],
1454 vel_m_s: [km(o1[1], "vx")?, km(o2[1], "vy")?, km(o3[1], "vz")?],
1455 acc_m_s2: [km(o1[2], "ax")?, km(o2[2], "ay")?, km(o3[2], "az")?],
1456 clk_bias,
1457 gamma_n,
1458 sv_health: g(o1[3], "health")?,
1459 freq_channel: glonass_frequency_channel(g(o2[3], "frequency channel")?, &sat)?,
1460 })
1461}
1462
1463pub fn parse_glonass(text: &str) -> Result<Vec<GlonassRecord>, NavParseError> {
1471 Ok(parse_glonass_lenient(text)?.records)
1472}
1473
1474pub fn parse_glonass_lenient(text: &str) -> Result<GlonassParse, NavParseError> {
1483 let mut lines = text.lines();
1484 verify_and_skip_header(&mut lines)?;
1485 let mut blocks: Vec<Vec<&str>> = Vec::new();
1486 for line in lines {
1487 if is_record_start(line) {
1488 blocks.push(vec![line]);
1489 } else if let Some(last) = blocks.last_mut() {
1490 last.push(line);
1491 }
1492 }
1493 let mut out = GlonassParse::default();
1494 for block in blocks.iter().filter(|b| b[0].starts_with('R')) {
1495 let sat = block[0].get(0..3).unwrap_or("").trim();
1501 if sat.parse::<GnssSatelliteId>().is_err() {
1502 out.skipped.push(SkippedGlonass {
1503 token: sat.to_string(),
1504 });
1505 continue;
1506 }
1507 out.records.push(parse_glonass_block(block)?);
1508 }
1509 Ok(out)
1510}
1511
1512fn verify_and_skip_header<'a, I>(lines: &mut I) -> Result<RinexVersion, NavParseError>
1516where
1517 I: Iterator<Item = &'a str>,
1518{
1519 let mut version_seen: Option<RinexVersion> = None;
1520 for line in lines.by_ref() {
1521 if line.contains("RINEX VERSION / TYPE") {
1522 let version = line.get(0..9).unwrap_or("").trim();
1524 let detected = parse_rinex_version(version);
1525 let is_nav = line.get(20..21) == Some("N");
1526 match (detected, is_nav) {
1527 (Some(v), true) => version_seen = Some(v),
1528 _ => {
1529 return Err(NavParseError::UnsupportedHeader(
1530 line.trim_end().to_string(),
1531 ))
1532 }
1533 }
1534 }
1535 if line.contains("END OF HEADER") {
1536 return version_seen.ok_or_else(|| {
1537 NavParseError::UnsupportedHeader("no RINEX VERSION / TYPE".to_string())
1538 });
1539 }
1540 }
1541 Err(NavParseError::MissingHeaderEnd)
1542}
1543
1544fn parse_rinex_version(version: &str) -> Option<RinexVersion> {
1545 let (major, minor) = version.split_once('.')?;
1546 let major = major.trim().parse::<u8>().ok()?;
1547 if !matches!(major, 3 | 4) {
1548 return None;
1549 }
1550 let minor_digits = minor
1551 .chars()
1552 .take_while(char::is_ascii_digit)
1553 .collect::<String>();
1554 if minor_digits.is_empty() {
1555 return None;
1556 }
1557 let minor = minor_digits.parse::<u8>().ok()?;
1558 Some(RinexVersion { major, minor })
1559}
1560
1561fn is_record_start(line: &str) -> bool {
1562 let Some(token) = line.get(0..3) else {
1563 return false;
1564 };
1565 let b = token.as_bytes();
1566 let prn = token[1..].trim();
1567 b[0].is_ascii_alphabetic()
1568 && (1..=2).contains(&prn.len())
1569 && prn.bytes().all(|byte| byte.is_ascii_digit())
1570}
1571
1572fn orbit_row(line: &str) -> [Option<f64>; 4] {
1574 [
1575 parse_f64(line, 4, 23),
1576 parse_f64(line, 23, 42),
1577 parse_f64(line, 42, 61),
1578 parse_f64(line, 61, 80),
1579 ]
1580}
1581
1582fn raw_orbit_field(line: &str, field_index: usize) -> &str {
1583 const RANGES: [(usize, usize); 4] = [(4, 23), (23, 42), (42, 61), (61, 80)];
1584 let (start, end) = RANGES[field_index];
1585 raw_field(line, start, end)
1586}
1587
1588#[derive(Debug, Clone, Copy)]
1589struct ClockReferenceEpoch {
1590 week: u32,
1591 sow: f64,
1592}
1593
1594fn parse_keplerian_block(
1595 block: &[&str],
1596 message_override: Option<NavMessage>,
1597 version: RinexVersion,
1598) -> Result<BroadcastRecord, NavParseError> {
1599 let l0 = block.first().copied().unwrap_or("");
1600 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1601 if block.len() < 8 {
1602 return Err(NavParseError::TruncatedRecord(sat));
1603 }
1604 let bad = |what: &'static str| NavParseError::BadField {
1605 satellite: sat.clone(),
1606 field: what,
1607 };
1608
1609 let letter = l0
1610 .as_bytes()
1611 .first()
1612 .copied()
1613 .map(|b| b as char)
1614 .ok_or_else(|| bad("system"))?;
1615 let system = GnssSystem::from_letter(letter).ok_or_else(|| bad("system"))?;
1616 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1617
1618 let time_scale = broadcast_time_scale(system);
1620 let toc_epoch = parse_toc(l0, &sat, time_scale)?;
1621 let toc_sow = toc_epoch.sow;
1622 let af0 = parse_f64(l0, 23, 42).ok_or_else(|| bad("af0"))?;
1623 let af1 = parse_f64(l0, 42, 61).ok_or_else(|| bad("af1"))?;
1624 let af2 = parse_f64(l0, 61, 80).ok_or_else(|| bad("af2"))?;
1625
1626 let o1 = orbit_row(block[1]);
1627 let o2 = orbit_row(block[2]);
1628 let o3 = orbit_row(block[3]);
1629 let o4 = orbit_row(block[4]);
1630 let o5 = orbit_row(block[5]);
1631 let o6 = orbit_row(block[6]);
1632
1633 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1634
1635 let elements = KeplerianElements {
1636 crs: g(o1[1], "crs")?,
1637 delta_n: g(o1[2], "deltaN")?,
1638 m0: g(o1[3], "m0")?,
1639 cuc: g(o2[0], "cuc")?,
1640 e: g(o2[1], "e")?,
1641 cus: g(o2[2], "cus")?,
1642 sqrt_a: g(o2[3], "sqrtA")?,
1643 toe_sow: g(o3[0], "toe")?,
1644 cic: g(o3[1], "cic")?,
1645 omega0: g(o3[2], "omega0")?,
1646 cis: g(o3[3], "cis")?,
1647 i0: g(o4[0], "i0")?,
1648 crc: g(o4[1], "crc")?,
1649 omega: g(o4[2], "omega")?,
1650 omega_dot: g(o4[3], "omegaDot")?,
1651 idot: g(o5[0], "idot")?,
1652 };
1653 let clock = ClockPolynomial {
1654 af0,
1655 af1,
1656 af2,
1657 toc_sow,
1658 };
1659
1660 let week = finite_integral_u32(g(o5[2], "week")?, "week", &sat)?;
1661 let toe = GnssWeekTow::new(time_scale, week, elements.toe_sow)
1662 .and_then(GnssWeekTow::normalized)
1663 .map_err(|_| bad("toe"))?;
1664 let toc = GnssWeekTow::new(time_scale, toc_epoch.week, clock.toc_sow)
1665 .and_then(GnssWeekTow::normalized)
1666 .map_err(|_| bad("toc"))?;
1667 let message = if let Some(message) = message_override {
1668 message
1669 } else {
1670 match system {
1671 GnssSystem::Galileo => galileo_message(g(o5[1], "data sources")?, &sat)?,
1672 GnssSystem::BeiDou => {
1673 if is_beidou_geo(satellite_id) {
1674 NavMessage::BeidouD2
1675 } else {
1676 NavMessage::BeidouD1
1677 }
1678 }
1679 _ => NavMessage::GpsLnav,
1680 }
1681 };
1682 let issue_of_data = BroadcastIssue {
1683 issue: finite_integral_u32(g(o1[0], "issue of data")?, "issue of data", &sat)?,
1684 message,
1685 };
1686
1687 let sv_accuracy_m = g(o6[0], "accuracy")?;
1688 let sv_health = g(o6[1], "health")?;
1689 let group_delays = match system {
1690 GnssSystem::Gps => BroadcastGroupDelays::gps_lnav(g(o6[2], "gps tgd")?),
1691 GnssSystem::Galileo => {
1695 BroadcastGroupDelays::galileo(g(o6[2], "bgd e5a/e1")?, g(o6[3], "bgd e5b/e1")?)
1696 }
1697 GnssSystem::BeiDou => {
1698 BroadcastGroupDelays::beidou(g(o6[2], "beidou tgd1")?, g(o6[3], "beidou tgd2")?)
1699 }
1700 _ => BroadcastGroupDelays::default(),
1701 };
1702
1703 let fit_interval_s = match system {
1706 GnssSystem::Gps => {
1707 Some(gps_fit_interval_s(block[7], version).map_err(|()| bad("fit interval"))?)
1708 }
1709 _ => None,
1710 };
1711
1712 Ok(BroadcastRecord {
1713 satellite_id,
1714 message,
1715 issue_of_data,
1716 week,
1717 toe,
1718 toc,
1719 elements,
1720 clock,
1721 group_delays,
1722 cnav: None,
1723 sv_health,
1724 sv_accuracy_m,
1725 fit_interval_s,
1726 })
1727}
1728
1729fn parse_cnav_block(block: &[&str], message: NavMessage) -> Result<BroadcastRecord, NavParseError> {
1730 let l0 = block.first().copied().unwrap_or("");
1731 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1732 let is_cnav2 = matches!(message, NavMessage::GpsCnav2 | NavMessage::QzssCnav2);
1733 let required_lines = if is_cnav2 { 10 } else { 9 };
1734 if block.len() < required_lines {
1735 return Err(NavParseError::TruncatedRecord(sat));
1736 }
1737 let bad = |what: &'static str| NavParseError::BadField {
1738 satellite: sat.clone(),
1739 field: what,
1740 };
1741
1742 let letter = l0
1743 .as_bytes()
1744 .first()
1745 .copied()
1746 .map(|b| b as char)
1747 .ok_or_else(|| bad("system"))?;
1748 GnssSystem::from_letter(letter).ok_or_else(|| bad("system"))?;
1749 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1750 let toc_epoch = parse_toc(l0, &sat, TimeScale::Gpst)?;
1751 let af0 = parse_f64(l0, 23, 42).ok_or_else(|| bad("af0"))?;
1752 let af1 = parse_f64(l0, 42, 61).ok_or_else(|| bad("af1"))?;
1753 let af2 = parse_f64(l0, 61, 80).ok_or_else(|| bad("af2"))?;
1754
1755 let o1 = orbit_row(block[1]);
1756 let o2 = orbit_row(block[2]);
1757 let o3 = orbit_row(block[3]);
1758 let o4 = orbit_row(block[4]);
1759 let o5 = orbit_row(block[5]);
1760 let o6 = orbit_row(block[6]);
1761 let o8 = orbit_row(block[8]);
1762 let o9 = if is_cnav2 {
1763 Some(orbit_row(block[9]))
1764 } else {
1765 None
1766 };
1767
1768 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1769 let elements = KeplerianElements {
1770 crs: g(o1[1], "crs")?,
1771 delta_n: g(o1[2], "deltaN0")?,
1772 m0: g(o1[3], "m0")?,
1773 cuc: g(o2[0], "cuc")?,
1774 e: g(o2[1], "e")?,
1775 cus: g(o2[2], "cus")?,
1776 sqrt_a: g(o2[3], "sqrtA0")?,
1777 toe_sow: toc_epoch.sow,
1778 cic: g(o3[1], "cic")?,
1779 omega0: g(o3[2], "omega0")?,
1780 cis: g(o3[3], "cis")?,
1781 i0: g(o4[0], "i0")?,
1782 crc: g(o4[1], "crc")?,
1783 omega: g(o4[2], "omega")?,
1784 omega_dot: g(o4[3], "omegaDot")?,
1785 idot: g(o5[0], "idot")?,
1786 };
1787 let clock = ClockPolynomial {
1788 af0,
1789 af1,
1790 af2,
1791 toc_sow: toc_epoch.sow,
1792 };
1793
1794 let week = toc_epoch.week;
1795 let toe = GnssWeekTow::new(TimeScale::Gpst, week, elements.toe_sow)
1796 .and_then(GnssWeekTow::normalized)
1797 .map_err(|_| bad("toe"))?;
1798 let toc = GnssWeekTow::new(TimeScale::Gpst, week, clock.toc_sow)
1799 .and_then(GnssWeekTow::normalized)
1800 .map_err(|_| bad("toc"))?;
1801 let wn_op = finite_integral_u32(
1802 g(if is_cnav2 { o9.unwrap()[1] } else { o8[1] }, "wn_op")?,
1803 "wn_op",
1804 &sat,
1805 )?;
1806 let top_sow = g(o3[0], "top")?;
1807 let top = GnssWeekTow::new(TimeScale::Gpst, wn_op, top_sow)
1808 .and_then(GnssWeekTow::normalized)
1809 .map_err(|_| bad("top"))?;
1810 let ura_ed_index = finite_integral_i8(g(o6[0], "ura_ed")?, "ura_ed", -16, 15, &sat)?;
1811 let ura_ned0_index = finite_integral_i8(g(o5[2], "ura_ned0")?, "ura_ned0", -16, 15, &sat)?;
1812 let ura_ned1_index = finite_integral_u8(g(o5[3], "ura_ned1")?, "ura_ned1", 0, 7, &sat)?;
1813 let ura_ned2_index = finite_integral_u8(g(o6[3], "ura_ned2")?, "ura_ned2", 0, 7, &sat)?;
1814 let health_max = if is_cnav2 { 1 } else { 7 };
1815 let sv_health = f64::from(finite_integral_u8(
1816 g(o6[1], "health")?,
1817 "health",
1818 0,
1819 health_max,
1820 &sat,
1821 )?);
1822 let transmission_time_sow = g(if is_cnav2 { o9.unwrap()[0] } else { o8[0] }, "t_tm")?;
1823 let flags = optional_integral_u32(
1824 if is_cnav2 {
1825 raw_orbit_field(block[9], 2)
1826 } else {
1827 raw_orbit_field(block[8], 2)
1828 },
1829 "flags",
1830 &sat,
1831 )?;
1832
1833 let tgd = optional_cnav_delay(raw_orbit_field(block[6], 2), "tgd", &sat)?;
1834 let isc_l1ca = optional_cnav_delay(raw_orbit_field(block[7], 0), "isc_l1ca", &sat)?;
1835 let isc_l2c = optional_cnav_delay(raw_orbit_field(block[7], 1), "isc_l2c", &sat)?;
1836 let isc_l5i5 = optional_cnav_delay(raw_orbit_field(block[7], 2), "isc_l5i5", &sat)?;
1837 let isc_l5q5 = optional_cnav_delay(raw_orbit_field(block[7], 3), "isc_l5q5", &sat)?;
1838 let (isc_l1cd, isc_l1cp) = if is_cnav2 {
1839 (
1840 optional_cnav_delay(raw_orbit_field(block[8], 0), "isc_l1cd", &sat)?,
1841 optional_cnav_delay(raw_orbit_field(block[8], 1), "isc_l1cp", &sat)?,
1842 )
1843 } else {
1844 (None, None)
1845 };
1846
1847 let cnav = CnavParameters {
1848 adot_m_s: g(o1[0], "adot")?,
1849 delta_n0_dot_rad_s2: g(o5[1], "deltaN0Dot")?,
1850 top,
1851 ura_ed_index,
1852 ura_ned0_index,
1853 ura_ned1_index,
1854 ura_ned2_index,
1855 transmission_time_sow,
1856 flags,
1857 };
1858 let sv_accuracy_m = cnav_ura_nominal_m(ura_ed_index).unwrap_or(8192.0);
1859 let issue = (elements.toe_sow / 300.0).round() as u32;
1860
1861 Ok(BroadcastRecord {
1862 satellite_id,
1863 message,
1864 issue_of_data: BroadcastIssue { issue, message },
1865 week,
1866 toe,
1867 toc,
1868 elements,
1869 clock,
1870 group_delays: BroadcastGroupDelays::cnav(
1871 tgd, isc_l1ca, isc_l2c, isc_l5i5, isc_l5q5, isc_l1cd, isc_l1cp,
1872 ),
1873 cnav: Some(cnav),
1874 sv_health,
1875 sv_accuracy_m,
1876 fit_interval_s: Some(3.0 * SECONDS_PER_HOUR),
1877 })
1878}
1879
1880fn gps_fit_interval_s(orbit7: &str, version: RinexVersion) -> Result<f64, ()> {
1892 let value = match field(orbit7, 23, 42) {
1893 None => 0.0,
1894 Some(_) => parse_f64(orbit7, 23, 42).ok_or(())?,
1895 };
1896 if value == 0.0 {
1897 Ok(GPS_NOMINAL_FIT_INTERVAL_S)
1898 } else if version.gps_fit_interval_uses_legacy_flag() && value == 1.0 {
1899 Ok(GPS_LEGACY_EXTENDED_FIT_INTERVAL_S)
1900 } else {
1901 Ok(value * SECONDS_PER_HOUR)
1902 }
1903}
1904
1905fn galileo_message(data_sources: f64, sat: &str) -> Result<NavMessage, NavParseError> {
1909 let word = finite_integral_u32(data_sources, "data sources", sat)?;
1910 if word & 0b010 != 0 {
1911 Ok(NavMessage::GalileoFnav)
1912 } else if word & 0b101 != 0 {
1913 Ok(NavMessage::GalileoInav)
1914 } else {
1915 Ok(NavMessage::GalileoInav)
1917 }
1918}
1919
1920fn finite_integral_u32(value: f64, field: &'static str, sat: &str) -> Result<u32, NavParseError> {
1921 validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
1922 if value < 0.0 || value > f64::from(u32::MAX) || value.trunc() != value {
1923 return Err(NavParseError::BadField {
1924 satellite: sat.to_string(),
1925 field,
1926 });
1927 }
1928 Ok(value as u32)
1929}
1930
1931fn finite_integral_i8(
1932 value: f64,
1933 field: &'static str,
1934 min: i8,
1935 max: i8,
1936 sat: &str,
1937) -> Result<i8, NavParseError> {
1938 validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
1939 if value < f64::from(min) || value > f64::from(max) || value.trunc() != value {
1940 return Err(NavParseError::BadField {
1941 satellite: sat.to_string(),
1942 field,
1943 });
1944 }
1945 Ok(value as i8)
1946}
1947
1948fn finite_integral_u8(
1949 value: f64,
1950 field: &'static str,
1951 min: u8,
1952 max: u8,
1953 sat: &str,
1954) -> Result<u8, NavParseError> {
1955 validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
1956 if value < f64::from(min) || value > f64::from(max) || value.trunc() != value {
1957 return Err(NavParseError::BadField {
1958 satellite: sat.to_string(),
1959 field,
1960 });
1961 }
1962 Ok(value as u8)
1963}
1964
1965fn optional_integral_u32(
1966 raw: &str,
1967 field: &'static str,
1968 sat: &str,
1969) -> Result<Option<u32>, NavParseError> {
1970 if raw.trim().is_empty() {
1971 return Ok(None);
1972 }
1973 let value =
1974 validate::strict_f64(raw, field).map_err(|error| map_record_field_error(error, sat))?;
1975 finite_integral_u32(value, field, sat).map(Some)
1976}
1977
1978fn optional_cnav_delay(
1979 raw: &str,
1980 field: &'static str,
1981 sat: &str,
1982) -> Result<Option<f64>, NavParseError> {
1983 if raw.trim().is_empty() {
1984 return Ok(None);
1985 }
1986 let value =
1987 validate::strict_f64(raw, field).map_err(|error| map_record_field_error(error, sat))?;
1988 if !write::d19_12_representable(value) {
1989 return Err(NavParseError::BadField {
1990 satellite: sat.to_string(),
1991 field,
1992 });
1993 }
1994 let mut rendered = String::new();
1995 write::push_d19_12(&mut rendered, value);
1996 let mut sentinel = String::new();
1997 write::push_d19_12(&mut sentinel, -4096.0 * 2.0_f64.powi(-35));
1998 if rendered == sentinel {
1999 Ok(None)
2000 } else {
2001 Ok(Some(value))
2002 }
2003}
2004
2005fn glonass_frequency_channel(value: f64, sat: &str) -> Result<i32, NavParseError> {
2006 const FIELD: &str = "frequency channel";
2007 validate::finite(value, FIELD).map_err(|error| map_record_field_error(error, sat))?;
2008 let channel = value as i32;
2009 if value.trunc() != value || !valid_glonass_frequency_channel(channel) {
2010 return Err(NavParseError::BadField {
2011 satellite: sat.to_string(),
2012 field: FIELD,
2013 });
2014 }
2015 Ok(channel)
2016}
2017
2018fn strict_header_f64(
2019 line: &str,
2020 start: usize,
2021 end: usize,
2022 field: &'static str,
2023) -> Result<f64, NavParseError> {
2024 validate::strict_f64(raw_field(line, start, end), field).map_err(map_header_field_error)
2025}
2026
2027fn strict_header_integer_f64(
2028 line: &str,
2029 start: usize,
2030 end: usize,
2031 field: &'static str,
2032) -> Result<f64, NavParseError> {
2033 let value = strict_header_f64(line, start, end, field)?;
2034 if value.trunc() != value {
2035 return Err(NavParseError::BadHeaderField { field });
2036 }
2037 Ok(value)
2038}
2039
2040fn strict_record_int<T>(
2041 line: &str,
2042 start: usize,
2043 end: usize,
2044 field: &'static str,
2045 satellite: &str,
2046) -> Result<T, NavParseError>
2047where
2048 T: core::str::FromStr,
2049{
2050 validate::strict_int::<T>(raw_field(line, start, end), field)
2051 .map_err(|error| map_record_field_error(error, satellite))
2052}
2053
2054fn map_record_field_error(error: FieldError, satellite: &str) -> NavParseError {
2055 NavParseError::BadField {
2056 satellite: satellite.to_string(),
2057 field: error.field(),
2058 }
2059}
2060
2061fn map_header_field_error(error: FieldError) -> NavParseError {
2062 NavParseError::BadHeaderField {
2063 field: error.field(),
2064 }
2065}
2066
2067fn parse_toc(
2070 l0: &str,
2071 sat: &str,
2072 time_scale: TimeScale,
2073) -> Result<ClockReferenceEpoch, NavParseError> {
2074 let year = strict_record_int::<i64>(l0, 4, 8, "toc epoch", sat)?;
2075 let month = strict_record_int::<i64>(l0, 9, 11, "toc epoch", sat)?;
2076 let day = strict_record_int::<i64>(l0, 12, 14, "toc epoch", sat)?;
2077 let hour = strict_record_int::<i64>(l0, 15, 17, "toc epoch", sat)?;
2078 let minute = strict_record_int::<i64>(l0, 18, 20, "toc epoch", sat)?;
2079 let second = strict_record_int::<i64>(l0, 21, 23, "toc epoch", sat)?;
2080 let civil = validate::civil_datetime_with_second_policy(
2081 year,
2082 month,
2083 day,
2084 hour,
2085 minute,
2086 second as f64,
2087 validate::CivilSecondPolicy::Continuous,
2088 )
2089 .map_err(|_| NavParseError::BadField {
2090 satellite: sat.to_string(),
2091 field: "toc epoch",
2092 })?;
2093 let month = i64::from(civil.month);
2094 let day = i64::from(civil.day);
2095 let week = gnss::week_from_calendar(time_scale, civil.year, month, day).ok_or_else(|| {
2096 NavParseError::BadField {
2097 satellite: sat.to_string(),
2098 field: "toc epoch",
2099 }
2100 })?;
2101 let sow = gnss::seconds_of_week_from_calendar(
2102 civil.year,
2103 month,
2104 day,
2105 i64::from(civil.hour),
2106 i64::from(civil.minute),
2107 civil.second as i64,
2108 );
2109 Ok(ClockReferenceEpoch { week, sow })
2110}
2111
2112#[cfg(all(test, sidereon_repo_tests))]
2113mod tests;