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