1mod store;
27pub use store::BroadcastStore;
28
29mod write;
30pub use write::encode_nav;
31
32use crate::astro::time::model::{GnssWeekTow, TimeScale};
33use crate::astro::time::{civil, gnss};
34use crate::broadcast::{ClockPolynomial, ConstellationConstants, KeplerianElements};
35use crate::constants::{KM_TO_M, SECONDS_PER_HOUR};
36use crate::format::columns::{field, raw_field};
37use crate::id::{GnssSatelliteId, GnssSystem};
38use crate::ionex::GalileoNequickCoeffs;
39use crate::validate::{self, FieldError};
40
41fn parse_f64(line: &str, start: usize, end: usize) -> Option<f64> {
46 let value = crate::format::columns::fortran_f64(line, start, end, "numeric field")?;
47 write::d19_12_representable(value).then_some(value)
54}
55
56pub(crate) const MAX_EPHEMERIS_AGE_S: f64 = 4.0 * SECONDS_PER_HOUR;
63
64pub(crate) const GLONASS_MAX_AGE_S: f64 = 15.0 * 60.0;
68const GPS_NOMINAL_FIT_INTERVAL_S: f64 = 4.0 * SECONDS_PER_HOUR;
69const GPS_LEGACY_EXTENDED_FIT_INTERVAL_S: f64 = 8.0 * SECONDS_PER_HOUR;
70const GLONASS_FREQ_CHANNEL_MIN: i32 = -7;
71const GLONASS_FREQ_CHANNEL_MAX: i32 = 6;
72
73pub(crate) fn valid_glonass_frequency_channel(channel: i32) -> bool {
74 (GLONASS_FREQ_CHANNEL_MIN..=GLONASS_FREQ_CHANNEL_MAX).contains(&channel)
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78struct RinexVersion {
79 major: u8,
80 minor: u8,
81}
82
83impl RinexVersion {
84 fn gps_fit_interval_uses_legacy_flag(self) -> bool {
85 self.major == 3 && self.minor <= 2
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum NavMessage {
92 GpsLnav,
94 GalileoInav,
96 GalileoFnav,
98 BeidouD1,
100 BeidouD2,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct BroadcastIssue {
107 pub issue: u32,
109 pub message: NavMessage,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum BroadcastGroupDelayTerm {
116 GpsTgd,
118 GalileoBgdE5aE1,
120 GalileoBgdE5bE1,
122 BeidouTgd1,
124 BeidouTgd2,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Default)]
130pub struct BroadcastGroupDelays {
131 pub gps_tgd_s: Option<f64>,
133 pub galileo_bgd_e5a_e1_s: Option<f64>,
135 pub galileo_bgd_e5b_e1_s: Option<f64>,
137 pub beidou_tgd1_s: Option<f64>,
139 pub beidou_tgd2_s: Option<f64>,
141}
142
143impl BroadcastGroupDelays {
144 pub const fn gps_lnav(tgd_s: f64) -> Self {
146 Self {
147 gps_tgd_s: Some(tgd_s),
148 galileo_bgd_e5a_e1_s: None,
149 galileo_bgd_e5b_e1_s: None,
150 beidou_tgd1_s: None,
151 beidou_tgd2_s: None,
152 }
153 }
154
155 pub const fn galileo(bgd_e5a_e1_s: f64, bgd_e5b_e1_s: f64) -> Self {
157 Self {
158 gps_tgd_s: None,
159 galileo_bgd_e5a_e1_s: Some(bgd_e5a_e1_s),
160 galileo_bgd_e5b_e1_s: Some(bgd_e5b_e1_s),
161 beidou_tgd1_s: None,
162 beidou_tgd2_s: None,
163 }
164 }
165
166 pub const fn beidou(tgd1_s: f64, tgd2_s: f64) -> Self {
168 Self {
169 gps_tgd_s: None,
170 galileo_bgd_e5a_e1_s: None,
171 galileo_bgd_e5b_e1_s: None,
172 beidou_tgd1_s: Some(tgd1_s),
173 beidou_tgd2_s: Some(tgd2_s),
174 }
175 }
176
177 pub const fn get(&self, term: BroadcastGroupDelayTerm) -> Option<f64> {
179 match term {
180 BroadcastGroupDelayTerm::GpsTgd => self.gps_tgd_s,
181 BroadcastGroupDelayTerm::GalileoBgdE5aE1 => self.galileo_bgd_e5a_e1_s,
182 BroadcastGroupDelayTerm::GalileoBgdE5bE1 => self.galileo_bgd_e5b_e1_s,
183 BroadcastGroupDelayTerm::BeidouTgd1 => self.beidou_tgd1_s,
184 BroadcastGroupDelayTerm::BeidouTgd2 => self.beidou_tgd2_s,
185 }
186 }
187
188 pub const fn for_message(self, system: GnssSystem, message: NavMessage) -> Option<f64> {
193 match (system, message) {
194 (GnssSystem::Gps, NavMessage::GpsLnav) => self.get(BroadcastGroupDelayTerm::GpsTgd),
195 (GnssSystem::Galileo, NavMessage::GalileoFnav) => {
196 self.get(BroadcastGroupDelayTerm::GalileoBgdE5aE1)
197 }
198 (GnssSystem::Galileo, NavMessage::GalileoInav) => {
199 self.get(BroadcastGroupDelayTerm::GalileoBgdE5bE1)
200 }
201 (GnssSystem::BeiDou, NavMessage::BeidouD1 | NavMessage::BeidouD2) => {
202 self.get(BroadcastGroupDelayTerm::BeidouTgd1)
203 }
204 _ => None,
205 }
206 }
207}
208
209pub fn is_beidou_geo(sat: GnssSatelliteId) -> bool {
212 sat.system == GnssSystem::BeiDou && (sat.prn <= 5 || (59..=61).contains(&sat.prn))
213}
214
215#[derive(Debug, Clone, Copy, PartialEq)]
219pub struct KlobucharAlphaBeta {
220 pub alpha: [f64; 4],
222 pub beta: [f64; 4],
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Default)]
233pub struct IonoCorrections {
234 pub gps: Option<KlobucharAlphaBeta>,
236 pub beidou: Option<KlobucharAlphaBeta>,
238 pub galileo: Option<GalileoNequickCoeffs>,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq)]
246pub struct GlonassRecord {
247 pub satellite_id: GnssSatelliteId,
249 pub toe_utc_j2000_s: f64,
252 pub pos_m: [f64; 3],
254 pub vel_m_s: [f64; 3],
256 pub acc_m_s2: [f64; 3],
258 pub clk_bias: f64,
260 pub gamma_n: f64,
262 pub sv_health: f64,
264 pub freq_channel: i32,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct SkippedGlonass {
273 pub token: String,
275}
276
277#[derive(Debug, Clone, PartialEq, Default)]
285pub struct GlonassParse {
286 pub records: Vec<GlonassRecord>,
288 pub skipped: Vec<SkippedGlonass>,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq)]
294pub struct BroadcastRecord {
295 pub satellite_id: GnssSatelliteId,
297 pub message: NavMessage,
299 pub issue_of_data: BroadcastIssue,
301 pub week: u32,
303 pub toe: GnssWeekTow,
305 pub toc: GnssWeekTow,
307 pub elements: KeplerianElements,
309 pub clock: ClockPolynomial,
311 pub group_delays: BroadcastGroupDelays,
313 pub sv_health: f64,
315 pub sv_accuracy_m: f64,
317 pub fit_interval_s: Option<f64>,
322}
323
324impl BroadcastRecord {
325 pub const fn time_scale(&self) -> TimeScale {
327 self.toe.system
328 }
329
330 pub const fn constants(&self) -> ConstellationConstants {
332 match self.satellite_id.system {
333 GnssSystem::Galileo => ConstellationConstants::GALILEO,
334 GnssSystem::BeiDou => ConstellationConstants::BEIDOU,
335 _ => ConstellationConstants::GPS,
337 }
338 }
339
340 pub fn broadcast_clock_group_delay_s(&self) -> f64 {
342 self.group_delays
343 .for_message(self.satellite_id.system, self.message)
344 .unwrap_or(0.0)
345 }
346
347 pub fn from_lnav(
381 decoded: &crate::navigation::lnav::LnavDecoded,
382 satellite_id: GnssSatelliteId,
383 full_week: u32,
384 ) -> Result<Self, LnavRecordError> {
385 if satellite_id.system != GnssSystem::Gps {
386 return Err(LnavRecordError::NotGps(satellite_id));
387 }
388
389 if i64::from(full_week % 1024) != decoded.week_number {
394 return Err(LnavRecordError::WeekMismatch {
395 full_week,
396 decoded_week: decoded.week_number,
397 });
398 }
399
400 let sv_accuracy_m = gps_ura_index_to_meters(decoded.ura_index)
401 .ok_or(LnavRecordError::NoUraPrediction(decoded.ura_index))?;
402 let fit_interval_s =
403 gps_fit_interval_from_flag(decoded.fit_interval_flag, decoded.iode, decoded.iodc)?;
404
405 const SEMICIRCLE_TO_RAD: f64 = core::f64::consts::PI;
408
409 let elements = KeplerianElements {
410 sqrt_a: decoded.sqrt_a,
411 e: decoded.eccentricity,
412 m0: decoded.m0 * SEMICIRCLE_TO_RAD,
413 delta_n: decoded.delta_n * SEMICIRCLE_TO_RAD,
414 omega0: decoded.omega0 * SEMICIRCLE_TO_RAD,
415 i0: decoded.i0 * SEMICIRCLE_TO_RAD,
416 omega: decoded.omega * SEMICIRCLE_TO_RAD,
417 omega_dot: decoded.omega_dot * SEMICIRCLE_TO_RAD,
418 idot: decoded.idot * SEMICIRCLE_TO_RAD,
419 cuc: decoded.cuc,
420 cus: decoded.cus,
421 crc: decoded.crc,
422 crs: decoded.crs,
423 cic: decoded.cic,
424 cis: decoded.cis,
425 toe_sow: decoded.toe as f64,
426 };
427 let clock = ClockPolynomial {
428 af0: decoded.af0,
429 af1: decoded.af1,
430 af2: decoded.af2,
431 toc_sow: decoded.toc as f64,
432 };
433
434 let toe = GnssWeekTow::new(TimeScale::Gpst, full_week, elements.toe_sow)
435 .and_then(GnssWeekTow::normalized)
436 .map_err(|_| LnavRecordError::InvalidEpoch("toe"))?;
437 let toc = GnssWeekTow::new(TimeScale::Gpst, full_week, clock.toc_sow)
438 .and_then(GnssWeekTow::normalized)
439 .map_err(|_| LnavRecordError::InvalidEpoch("toc"))?;
440
441 Ok(BroadcastRecord {
442 satellite_id,
443 message: NavMessage::GpsLnav,
444 issue_of_data: BroadcastIssue {
445 issue: decoded.iode as u32,
446 message: NavMessage::GpsLnav,
447 },
448 week: full_week,
449 toe,
450 toc,
451 elements,
452 clock,
453 group_delays: BroadcastGroupDelays::gps_lnav(decoded.tgd),
454 sv_health: decoded.sv_health as f64,
455 sv_accuracy_m,
456 fit_interval_s: Some(fit_interval_s),
457 })
458 }
459}
460
461fn gps_ura_index_to_meters(index: i64) -> Option<f64> {
467 let meters = match index {
468 0 => 2.4,
469 1 => 3.4,
470 2 => 4.85,
471 3 => 6.85,
472 4 => 9.65,
473 5 => 13.65,
474 6 => 24.0,
475 7 => 48.0,
476 8 => 96.0,
477 9 => 192.0,
478 10 => 384.0,
479 11 => 768.0,
480 12 => 1536.0,
481 13 => 3072.0,
482 14 => 6144.0,
483 _ => return None,
486 };
487 Some(meters)
488}
489
490const GPS_FIT_INTERVAL_6H_S: f64 = 6.0 * SECONDS_PER_HOUR;
491const GPS_FIT_INTERVAL_8H_S: f64 = 8.0 * SECONDS_PER_HOUR;
492const GPS_FIT_INTERVAL_14H_S: f64 = 14.0 * SECONDS_PER_HOUR;
493const GPS_FIT_INTERVAL_26H_S: f64 = 26.0 * SECONDS_PER_HOUR;
494
495fn gps_fit_interval_from_flag(
505 fit_interval_flag: i64,
506 iode: i64,
507 iodc: i64,
508) -> Result<f64, LnavRecordError> {
509 let unsupported = || LnavRecordError::FitIntervalUnsupported {
510 fit_interval_flag,
511 iode,
512 iodc,
513 };
514 match fit_interval_flag {
515 0 => Ok(GPS_NOMINAL_FIT_INTERVAL_S),
516 1 => {
517 if (0..240).contains(&iode) {
518 Ok(GPS_FIT_INTERVAL_6H_S)
522 } else if (240..=255).contains(&iode) {
523 match iodc {
525 240..=247 => Ok(GPS_FIT_INTERVAL_8H_S),
526 248..=255 | 496 => Ok(GPS_FIT_INTERVAL_14H_S),
527 497..=503 | 1021..=1023 => Ok(GPS_FIT_INTERVAL_26H_S),
528 _ => Err(unsupported()),
529 }
530 } else {
531 Err(unsupported())
532 }
533 }
534 _ => Err(unsupported()),
535 }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum LnavRecordError {
541 NotGps(GnssSatelliteId),
543 InvalidEpoch(&'static str),
545 WeekMismatch {
548 full_week: u32,
550 decoded_week: i64,
552 },
553 NoUraPrediction(i64),
555 FitIntervalUnsupported {
558 fit_interval_flag: i64,
560 iode: i64,
562 iodc: i64,
564 },
565}
566
567impl core::fmt::Display for LnavRecordError {
568 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
569 match self {
570 LnavRecordError::NotGps(sat) => {
571 write!(f, "LNAV is a GPS message; {sat} is not a GPS satellite")
572 }
573 LnavRecordError::InvalidEpoch(field) => {
574 write!(f, "derived {field} week/TOW is not representable")
575 }
576 LnavRecordError::WeekMismatch {
577 full_week,
578 decoded_week,
579 } => write!(
580 f,
581 "full_week {full_week} (week % 1024 = {}) disagrees with decoded 10-bit week {decoded_week}",
582 full_week % 1024
583 ),
584 LnavRecordError::NoUraPrediction(index) => {
585 write!(f, "URA index {index} carries no accuracy prediction")
586 }
587 LnavRecordError::FitIntervalUnsupported {
588 fit_interval_flag,
589 iode,
590 iodc,
591 } => write!(
592 f,
593 "fit interval flag {fit_interval_flag} with IODE {iode} / IODC {iodc} is not a defined curve-fit interval"
594 ),
595 }
596 }
597}
598
599impl std::error::Error for LnavRecordError {}
600
601fn broadcast_time_scale(system: GnssSystem) -> TimeScale {
602 match system {
603 GnssSystem::Galileo => TimeScale::Gst,
604 GnssSystem::BeiDou => TimeScale::Bdt,
605 _ => TimeScale::Gpst,
606 }
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
611pub enum NavParseError {
612 UnsupportedHeader(String),
614 MissingHeaderEnd,
616 TruncatedRecord(String),
618 BadField {
620 satellite: String,
622 field: &'static str,
624 },
625 BadHeaderField {
627 field: &'static str,
629 },
630}
631
632impl core::fmt::Display for NavParseError {
633 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
634 match self {
635 NavParseError::UnsupportedHeader(s) => write!(f, "unsupported RINEX NAV header: {s}"),
636 NavParseError::MissingHeaderEnd => write!(f, "no END OF HEADER line"),
637 NavParseError::TruncatedRecord(s) => write!(f, "truncated navigation record for {s}"),
638 NavParseError::BadField { satellite, field } => {
639 write!(f, "bad/missing {field} field in record for {satellite}")
640 }
641 NavParseError::BadHeaderField { field } => {
642 write!(f, "bad/missing {field} field in navigation header")
643 }
644 }
645 }
646}
647
648impl std::error::Error for NavParseError {}
649
650pub fn parse_nav(text: &str) -> Result<Vec<BroadcastRecord>, NavParseError> {
660 let mut lines = text.lines();
661 let version = verify_and_skip_header(&mut lines)?;
662 if version.major >= 4 {
663 parse_nav_v4(lines, version)
664 } else {
665 parse_nav_v3(lines, version)
666 }
667}
668
669fn parse_nav_v3<'a, I>(
672 lines: I,
673 version: RinexVersion,
674) -> Result<Vec<BroadcastRecord>, NavParseError>
675where
676 I: Iterator<Item = &'a str>,
677{
678 let mut blocks: Vec<Vec<&str>> = Vec::new();
679 for line in lines {
680 if is_record_start(line) {
681 blocks.push(vec![line]);
682 } else if let Some(last) = blocks.last_mut() {
683 last.push(line);
684 }
685 }
686
687 let mut records = Vec::new();
688 for block in &blocks {
689 let letter = block[0].as_bytes()[0] as char;
690 match GnssSystem::from_letter(letter) {
691 Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou) => {
692 records.push(parse_keplerian_block(block, None, version)?);
693 }
694 _ => {}
696 }
697 }
698 Ok(records)
699}
700
701fn parse_nav_v4<'a, I>(
710 lines: I,
711 version: RinexVersion,
712) -> Result<Vec<BroadcastRecord>, NavParseError>
713where
714 I: Iterator<Item = &'a str>,
715{
716 let frames = v4_frames(lines);
719 let mut records = Vec::new();
720 for (marker, body) in &frames {
721 let Some((frame_type, sv, msg_token)) = parse_v4_marker(marker) else {
722 continue;
723 };
724 if frame_type != "EPH" {
725 continue; }
727 let letter = sv.as_bytes().first().copied().map_or(' ', char::from);
728 let supported = matches!(
729 GnssSystem::from_letter(letter),
730 Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou)
731 );
732 if !supported {
733 continue; }
735 if let Some(message) = nav_message_from_v4_token(msg_token) {
738 validate_v4_ephemeris_marker(sv, message, body)?;
739 records.push(parse_keplerian_block(body, Some(message), version)?);
740 }
741 }
742 Ok(records)
743}
744
745fn v4_frames<'a, I>(lines: I) -> Vec<(&'a str, Vec<&'a str>)>
746where
747 I: Iterator<Item = &'a str>,
748{
749 let mut frames: Vec<(&str, Vec<&str>)> = Vec::new();
750 for line in lines {
751 if is_v4_frame_marker(line) {
752 frames.push((line, Vec::new()));
753 } else if let Some((_, body)) = frames.last_mut() {
754 body.push(line);
755 }
756 }
757 frames
758}
759
760fn is_v4_frame_marker(line: &str) -> bool {
762 line.starts_with("> ")
763}
764
765fn parse_v4_marker(line: &str) -> Option<(&str, &str, &str)> {
769 let rest = line.strip_prefix('>')?;
770 let mut fields = rest.split_whitespace();
771 let frame_type = fields.next()?;
772 let sv = fields.next()?;
773 let msg_token = fields.next()?;
774 Some((frame_type, sv, msg_token))
775}
776
777fn nav_message_from_v4_token(token: &str) -> Option<NavMessage> {
781 match token {
782 "LNAV" => Some(NavMessage::GpsLnav),
783 "INAV" => Some(NavMessage::GalileoInav),
784 "FNAV" => Some(NavMessage::GalileoFnav),
785 "D1" => Some(NavMessage::BeidouD1),
786 "D2" => Some(NavMessage::BeidouD2),
787 _ => None,
788 }
789}
790
791fn validate_v4_ephemeris_marker(
792 marker_sv: &str,
793 message: NavMessage,
794 body: &[&str],
795) -> Result<(), NavParseError> {
796 let Some(body_sv) = body
797 .first()
798 .and_then(|line| line.get(0..3))
799 .map(str::trim)
800 .filter(|sv| !sv.is_empty())
801 else {
802 return Ok(());
803 };
804
805 if marker_sv != body_sv {
806 return Err(NavParseError::BadField {
807 satellite: marker_sv.to_string(),
808 field: "frame marker",
809 });
810 }
811
812 let system = body_sv
813 .as_bytes()
814 .first()
815 .and_then(|b| GnssSystem::from_letter(*b as char))
816 .ok_or_else(|| NavParseError::BadField {
817 satellite: body_sv.to_string(),
818 field: "system",
819 })?;
820 if !nav_message_matches_system(message, system) {
821 return Err(NavParseError::BadField {
822 satellite: body_sv.to_string(),
823 field: "message",
824 });
825 }
826
827 Ok(())
828}
829
830fn nav_message_matches_system(message: NavMessage, system: GnssSystem) -> bool {
831 matches!(
832 (message, system),
833 (NavMessage::GpsLnav, GnssSystem::Gps)
834 | (
835 NavMessage::GalileoInav | NavMessage::GalileoFnav,
836 GnssSystem::Galileo,
837 )
838 | (
839 NavMessage::BeidouD1 | NavMessage::BeidouD2,
840 GnssSystem::BeiDou,
841 )
842 )
843}
844
845pub fn parse_iono_corrections(text: &str) -> Result<IonoCorrections, NavParseError> {
853 parse_iono_corrections_checked(text)
854}
855
856fn parse_iono_corrections_checked(text: &str) -> Result<IonoCorrections, NavParseError> {
857 let klobuchar_row = |line: &str| -> Result<[f64; 4], NavParseError> {
864 Ok([
865 strict_header_f64(line, 5, 17, "ionospheric correction")?,
866 strict_header_f64(line, 17, 29, "ionospheric correction")?,
867 strict_header_f64(line, 29, 41, "ionospheric correction")?,
868 strict_header_f64(line, 41, 53, "ionospheric correction")?,
869 ])
870 };
871 let nequick_row = |line: &str| -> Result<[f64; 3], NavParseError> {
876 Ok([
877 strict_header_f64(line, 5, 17, "ionospheric correction")?,
878 strict_header_f64(line, 17, 29, "ionospheric correction")?,
879 strict_header_f64(line, 29, 41, "ionospheric correction")?,
880 ])
881 };
882 let (mut gpsa, mut gpsb, mut bdsa, mut bdsb, mut gal) = (None, None, None, None, None);
883 for line in text.lines() {
884 if line.contains("END OF HEADER") {
885 break;
886 }
887 if !line.contains("IONOSPHERIC CORR") {
888 continue;
889 }
890 match line.get(0..4).map(str::trim) {
891 Some("GPSA") => gpsa = Some(klobuchar_row(line)?),
892 Some("GPSB") => gpsb = Some(klobuchar_row(line)?),
893 Some("BDSA") => bdsa = Some(klobuchar_row(line)?),
894 Some("BDSB") => bdsb = Some(klobuchar_row(line)?),
895 Some("GAL") => {
896 let row = nequick_row(line)?;
897 gal = Some(GalileoNequickCoeffs {
898 ai0: row[0],
899 ai1: row[1],
900 ai2: row[2],
901 });
902 }
903 _ => {}
904 }
905 }
906 let pair = |a: Option<[f64; 4]>, b: Option<[f64; 4]>| match (a, b) {
907 (Some(alpha), Some(beta)) => Some(KlobucharAlphaBeta { alpha, beta }),
908 _ => None,
909 };
910 let mut iono = IonoCorrections {
911 gps: pair(gpsa, gpsb),
912 beidou: pair(bdsa, bdsb),
913 galileo: gal,
914 };
915 parse_v4_body_iono_corrections(text, &mut iono)?;
916 Ok(iono)
917}
918
919fn parse_v4_body_iono_corrections(
920 text: &str,
921 iono: &mut IonoCorrections,
922) -> Result<(), NavParseError> {
923 let mut lines = text.lines();
924 for line in lines.by_ref() {
925 if line.contains("END OF HEADER") {
926 break;
927 }
928 }
929
930 for (marker, body) in v4_frames(lines) {
931 let Some((frame_type, sv, _msg_token)) = parse_v4_marker(marker) else {
932 continue;
933 };
934 if frame_type != "ION" {
935 continue;
936 }
937 let values = parse_v4_iono_values(sv, &body)?;
938 match sv
939 .as_bytes()
940 .first()
941 .and_then(|b| GnssSystem::from_letter(*b as char))
942 {
943 Some(GnssSystem::Gps) => {
944 iono.gps = Some(KlobucharAlphaBeta {
945 alpha: iono_values_4(&values, 0, sv)?,
946 beta: iono_values_4(&values, 4, sv)?,
947 });
948 }
949 Some(GnssSystem::BeiDou) => {
950 iono.beidou = Some(KlobucharAlphaBeta {
951 alpha: iono_values_4(&values, 0, sv)?,
952 beta: iono_values_4(&values, 4, sv)?,
953 });
954 }
955 Some(GnssSystem::Galileo) => {
956 let coeffs = iono_values_3(&values, 0, sv)?;
957 iono.galileo = Some(GalileoNequickCoeffs {
958 ai0: coeffs[0],
959 ai1: coeffs[1],
960 ai2: coeffs[2],
961 });
962 }
963 _ => {}
964 }
965 }
966 Ok(())
967}
968
969fn parse_v4_iono_values(sv: &str, body: &[&str]) -> Result<Vec<f64>, NavParseError> {
970 if body.is_empty() {
971 return Err(NavParseError::BadField {
972 satellite: sv.to_string(),
973 field: "ionospheric correction",
974 });
975 }
976
977 let mut values = Vec::new();
978 for (idx, line) in body.iter().enumerate() {
979 let ranges: &[(usize, usize)] = if idx == 0 {
980 &[(23, 42), (42, 61), (61, 80)]
981 } else {
982 &[(4, 23), (23, 42), (42, 61), (61, 80)]
983 };
984 for &(start, end) in ranges {
985 let raw = raw_field(line, start, end);
986 if raw.trim().is_empty() {
987 continue;
988 }
989 values.push(
990 validate::strict_f64(raw, "ionospheric correction")
991 .map_err(|error| map_record_field_error(error, sv))?,
992 );
993 }
994 }
995 Ok(values)
996}
997
998fn iono_values_4(values: &[f64], start: usize, sv: &str) -> Result<[f64; 4], NavParseError> {
999 let Some(slice) = values.get(start..start + 4) else {
1000 return Err(NavParseError::BadField {
1001 satellite: sv.to_string(),
1002 field: "ionospheric correction",
1003 });
1004 };
1005 Ok([slice[0], slice[1], slice[2], slice[3]])
1006}
1007
1008fn iono_values_3(values: &[f64], start: usize, sv: &str) -> Result<[f64; 3], NavParseError> {
1009 let Some(slice) = values.get(start..start + 3) else {
1010 return Err(NavParseError::BadField {
1011 satellite: sv.to_string(),
1012 field: "ionospheric correction",
1013 });
1014 };
1015 Ok([slice[0], slice[1], slice[2]])
1016}
1017
1018pub fn parse_leap_seconds(text: &str) -> Result<Option<f64>, NavParseError> {
1022 parse_leap_seconds_checked(text)
1023}
1024
1025fn parse_leap_seconds_checked(text: &str) -> Result<Option<f64>, NavParseError> {
1026 for line in text.lines() {
1027 if line.contains("END OF HEADER") {
1028 break;
1029 }
1030 if line.contains("LEAP SECONDS") {
1031 return strict_header_integer_f64(line, 0, 6, "leap seconds").map(Some);
1032 }
1033 }
1034 Ok(None)
1035}
1036
1037fn j2000_seconds_utc(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> f64 {
1042 civil::j2000_seconds(y as i32, mo as i32, d as i32, h as i32, mi as i32, s as f64)
1043}
1044
1045fn parse_glonass_epoch(l0: &str, sat: &str) -> Result<f64, NavParseError> {
1048 let year = strict_record_int::<i64>(l0, 4, 8, "epoch", sat)?;
1049 let month = strict_record_int::<i64>(l0, 9, 11, "epoch", sat)?;
1050 let day = strict_record_int::<i64>(l0, 12, 14, "epoch", sat)?;
1051 let hour = strict_record_int::<i64>(l0, 15, 17, "epoch", sat)?;
1052 let minute = strict_record_int::<i64>(l0, 18, 20, "epoch", sat)?;
1053 let second = strict_record_int::<i64>(l0, 21, 23, "epoch", sat)?;
1054 let civil = validate::civil_datetime_with_second_policy(
1055 year,
1056 month,
1057 day,
1058 hour,
1059 minute,
1060 second as f64,
1061 validate::CivilSecondPolicy::UtcLike,
1062 )
1063 .map_err(|_| NavParseError::BadField {
1064 satellite: sat.to_string(),
1065 field: "epoch",
1066 })?;
1067 Ok(j2000_seconds_utc(
1068 civil.year,
1069 i64::from(civil.month),
1070 i64::from(civil.day),
1071 i64::from(civil.hour),
1072 i64::from(civil.minute),
1073 civil.second as i64,
1074 ))
1075}
1076
1077fn parse_glonass_block(block: &[&str]) -> Result<GlonassRecord, NavParseError> {
1081 let l0 = block[0];
1082 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1083 if block.len() < 4 {
1084 return Err(NavParseError::TruncatedRecord(sat));
1085 }
1086 let bad = |what: &'static str| NavParseError::BadField {
1087 satellite: sat.clone(),
1088 field: what,
1089 };
1090 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1091 let toe_utc_j2000_s = parse_glonass_epoch(l0, &sat)?;
1092 let clk_bias = parse_f64(l0, 23, 42).ok_or_else(|| bad("clock bias"))?;
1093 let gamma_n = parse_f64(l0, 42, 61).ok_or_else(|| bad("gamma_n"))?;
1094 let o1 = orbit_row(block[1]);
1095 let o2 = orbit_row(block[2]);
1096 let o3 = orbit_row(block[3]);
1097 let km = |v: Option<f64>, what: &'static str| v.map(|x| x * KM_TO_M).ok_or_else(|| bad(what));
1098 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1099 Ok(GlonassRecord {
1100 satellite_id,
1101 toe_utc_j2000_s,
1102 pos_m: [km(o1[0], "x")?, km(o2[0], "y")?, km(o3[0], "z")?],
1103 vel_m_s: [km(o1[1], "vx")?, km(o2[1], "vy")?, km(o3[1], "vz")?],
1104 acc_m_s2: [km(o1[2], "ax")?, km(o2[2], "ay")?, km(o3[2], "az")?],
1105 clk_bias,
1106 gamma_n,
1107 sv_health: g(o1[3], "health")?,
1108 freq_channel: glonass_frequency_channel(g(o2[3], "frequency channel")?, &sat)?,
1109 })
1110}
1111
1112pub fn parse_glonass(text: &str) -> Result<Vec<GlonassRecord>, NavParseError> {
1120 Ok(parse_glonass_lenient(text)?.records)
1121}
1122
1123pub fn parse_glonass_lenient(text: &str) -> Result<GlonassParse, NavParseError> {
1132 let mut lines = text.lines();
1133 verify_and_skip_header(&mut lines)?;
1134 let mut blocks: Vec<Vec<&str>> = Vec::new();
1135 for line in lines {
1136 if is_record_start(line) {
1137 blocks.push(vec![line]);
1138 } else if let Some(last) = blocks.last_mut() {
1139 last.push(line);
1140 }
1141 }
1142 let mut out = GlonassParse::default();
1143 for block in blocks.iter().filter(|b| b[0].starts_with('R')) {
1144 let sat = block[0].get(0..3).unwrap_or("").trim();
1150 if sat.parse::<GnssSatelliteId>().is_err() {
1151 out.skipped.push(SkippedGlonass {
1152 token: sat.to_string(),
1153 });
1154 continue;
1155 }
1156 out.records.push(parse_glonass_block(block)?);
1157 }
1158 Ok(out)
1159}
1160
1161fn verify_and_skip_header<'a, I>(lines: &mut I) -> Result<RinexVersion, NavParseError>
1165where
1166 I: Iterator<Item = &'a str>,
1167{
1168 let mut version_seen: Option<RinexVersion> = None;
1169 for line in lines.by_ref() {
1170 if line.contains("RINEX VERSION / TYPE") {
1171 let version = line.get(0..9).unwrap_or("").trim();
1173 let detected = parse_rinex_version(version);
1174 let is_nav = line.get(20..21) == Some("N");
1175 match (detected, is_nav) {
1176 (Some(v), true) => version_seen = Some(v),
1177 _ => {
1178 return Err(NavParseError::UnsupportedHeader(
1179 line.trim_end().to_string(),
1180 ))
1181 }
1182 }
1183 }
1184 if line.contains("END OF HEADER") {
1185 return version_seen.ok_or_else(|| {
1186 NavParseError::UnsupportedHeader("no RINEX VERSION / TYPE".to_string())
1187 });
1188 }
1189 }
1190 Err(NavParseError::MissingHeaderEnd)
1191}
1192
1193fn parse_rinex_version(version: &str) -> Option<RinexVersion> {
1194 let (major, minor) = version.split_once('.')?;
1195 let major = major.trim().parse::<u8>().ok()?;
1196 if !matches!(major, 3 | 4) {
1197 return None;
1198 }
1199 let minor_digits = minor
1200 .chars()
1201 .take_while(char::is_ascii_digit)
1202 .collect::<String>();
1203 if minor_digits.is_empty() {
1204 return None;
1205 }
1206 let minor = minor_digits.parse::<u8>().ok()?;
1207 Some(RinexVersion { major, minor })
1208}
1209
1210fn is_record_start(line: &str) -> bool {
1211 let b = line.as_bytes();
1212 b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1].is_ascii_digit() && b[2].is_ascii_digit()
1213}
1214
1215fn orbit_row(line: &str) -> [Option<f64>; 4] {
1217 [
1218 parse_f64(line, 4, 23),
1219 parse_f64(line, 23, 42),
1220 parse_f64(line, 42, 61),
1221 parse_f64(line, 61, 80),
1222 ]
1223}
1224
1225#[derive(Debug, Clone, Copy)]
1226struct ClockReferenceEpoch {
1227 week: u32,
1228 sow: f64,
1229}
1230
1231fn parse_keplerian_block(
1232 block: &[&str],
1233 message_override: Option<NavMessage>,
1234 version: RinexVersion,
1235) -> Result<BroadcastRecord, NavParseError> {
1236 let l0 = block.first().copied().unwrap_or("");
1237 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1238 if block.len() < 8 {
1239 return Err(NavParseError::TruncatedRecord(sat));
1240 }
1241 let bad = |what: &'static str| NavParseError::BadField {
1242 satellite: sat.clone(),
1243 field: what,
1244 };
1245
1246 let letter = l0
1247 .as_bytes()
1248 .first()
1249 .copied()
1250 .map(|b| b as char)
1251 .ok_or_else(|| bad("system"))?;
1252 let system = GnssSystem::from_letter(letter).ok_or_else(|| bad("system"))?;
1253 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1254
1255 let time_scale = broadcast_time_scale(system);
1257 let toc_epoch = parse_toc(l0, &sat, time_scale)?;
1258 let toc_sow = toc_epoch.sow;
1259 let af0 = parse_f64(l0, 23, 42).ok_or_else(|| bad("af0"))?;
1260 let af1 = parse_f64(l0, 42, 61).ok_or_else(|| bad("af1"))?;
1261 let af2 = parse_f64(l0, 61, 80).ok_or_else(|| bad("af2"))?;
1262
1263 let o1 = orbit_row(block[1]);
1264 let o2 = orbit_row(block[2]);
1265 let o3 = orbit_row(block[3]);
1266 let o4 = orbit_row(block[4]);
1267 let o5 = orbit_row(block[5]);
1268 let o6 = orbit_row(block[6]);
1269
1270 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1271
1272 let elements = KeplerianElements {
1273 crs: g(o1[1], "crs")?,
1274 delta_n: g(o1[2], "deltaN")?,
1275 m0: g(o1[3], "m0")?,
1276 cuc: g(o2[0], "cuc")?,
1277 e: g(o2[1], "e")?,
1278 cus: g(o2[2], "cus")?,
1279 sqrt_a: g(o2[3], "sqrtA")?,
1280 toe_sow: g(o3[0], "toe")?,
1281 cic: g(o3[1], "cic")?,
1282 omega0: g(o3[2], "omega0")?,
1283 cis: g(o3[3], "cis")?,
1284 i0: g(o4[0], "i0")?,
1285 crc: g(o4[1], "crc")?,
1286 omega: g(o4[2], "omega")?,
1287 omega_dot: g(o4[3], "omegaDot")?,
1288 idot: g(o5[0], "idot")?,
1289 };
1290 let clock = ClockPolynomial {
1291 af0,
1292 af1,
1293 af2,
1294 toc_sow,
1295 };
1296
1297 let week = finite_integral_u32(g(o5[2], "week")?, "week", &sat)?;
1298 let toe = GnssWeekTow::new(time_scale, week, elements.toe_sow)
1299 .and_then(GnssWeekTow::normalized)
1300 .map_err(|_| bad("toe"))?;
1301 let toc = GnssWeekTow::new(time_scale, toc_epoch.week, clock.toc_sow)
1302 .and_then(GnssWeekTow::normalized)
1303 .map_err(|_| bad("toc"))?;
1304 let message = if let Some(message) = message_override {
1305 message
1306 } else {
1307 match system {
1308 GnssSystem::Galileo => galileo_message(g(o5[1], "data sources")?, &sat)?,
1309 GnssSystem::BeiDou => {
1310 if is_beidou_geo(satellite_id) {
1311 NavMessage::BeidouD2
1312 } else {
1313 NavMessage::BeidouD1
1314 }
1315 }
1316 _ => NavMessage::GpsLnav,
1317 }
1318 };
1319 let issue_of_data = BroadcastIssue {
1320 issue: finite_integral_u32(g(o1[0], "issue of data")?, "issue of data", &sat)?,
1321 message,
1322 };
1323
1324 let sv_accuracy_m = g(o6[0], "accuracy")?;
1325 let sv_health = g(o6[1], "health")?;
1326 let group_delays = match system {
1327 GnssSystem::Gps => BroadcastGroupDelays::gps_lnav(g(o6[2], "gps tgd")?),
1328 GnssSystem::Galileo => {
1332 BroadcastGroupDelays::galileo(g(o6[2], "bgd e5a/e1")?, g(o6[3], "bgd e5b/e1")?)
1333 }
1334 GnssSystem::BeiDou => {
1335 BroadcastGroupDelays::beidou(g(o6[2], "beidou tgd1")?, g(o6[3], "beidou tgd2")?)
1336 }
1337 _ => BroadcastGroupDelays::default(),
1338 };
1339
1340 let fit_interval_s = match system {
1343 GnssSystem::Gps => {
1344 Some(gps_fit_interval_s(block[7], version).map_err(|()| bad("fit interval"))?)
1345 }
1346 _ => None,
1347 };
1348
1349 Ok(BroadcastRecord {
1350 satellite_id,
1351 message,
1352 issue_of_data,
1353 week,
1354 toe,
1355 toc,
1356 elements,
1357 clock,
1358 group_delays,
1359 sv_health,
1360 sv_accuracy_m,
1361 fit_interval_s,
1362 })
1363}
1364
1365fn gps_fit_interval_s(orbit7: &str, version: RinexVersion) -> Result<f64, ()> {
1377 let value = match field(orbit7, 23, 42) {
1378 None => 0.0,
1379 Some(_) => parse_f64(orbit7, 23, 42).ok_or(())?,
1380 };
1381 if value == 0.0 {
1382 Ok(GPS_NOMINAL_FIT_INTERVAL_S)
1383 } else if version.gps_fit_interval_uses_legacy_flag() && value == 1.0 {
1384 Ok(GPS_LEGACY_EXTENDED_FIT_INTERVAL_S)
1385 } else {
1386 Ok(value * SECONDS_PER_HOUR)
1387 }
1388}
1389
1390fn galileo_message(data_sources: f64, sat: &str) -> Result<NavMessage, NavParseError> {
1394 let word = finite_integral_u32(data_sources, "data sources", sat)?;
1395 if word & 0b010 != 0 {
1396 Ok(NavMessage::GalileoFnav)
1397 } else if word & 0b101 != 0 {
1398 Ok(NavMessage::GalileoInav)
1399 } else {
1400 Ok(NavMessage::GalileoInav)
1402 }
1403}
1404
1405fn finite_integral_u32(value: f64, field: &'static str, sat: &str) -> Result<u32, NavParseError> {
1406 validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
1407 if value < 0.0 || value > f64::from(u32::MAX) || value.trunc() != value {
1408 return Err(NavParseError::BadField {
1409 satellite: sat.to_string(),
1410 field,
1411 });
1412 }
1413 Ok(value as u32)
1414}
1415
1416fn glonass_frequency_channel(value: f64, sat: &str) -> Result<i32, NavParseError> {
1417 const FIELD: &str = "frequency channel";
1418 validate::finite(value, FIELD).map_err(|error| map_record_field_error(error, sat))?;
1419 let channel = value as i32;
1420 if value.trunc() != value || !valid_glonass_frequency_channel(channel) {
1421 return Err(NavParseError::BadField {
1422 satellite: sat.to_string(),
1423 field: FIELD,
1424 });
1425 }
1426 Ok(channel)
1427}
1428
1429fn strict_header_f64(
1430 line: &str,
1431 start: usize,
1432 end: usize,
1433 field: &'static str,
1434) -> Result<f64, NavParseError> {
1435 validate::strict_f64(raw_field(line, start, end), field).map_err(map_header_field_error)
1436}
1437
1438fn strict_header_integer_f64(
1439 line: &str,
1440 start: usize,
1441 end: usize,
1442 field: &'static str,
1443) -> Result<f64, NavParseError> {
1444 let value = strict_header_f64(line, start, end, field)?;
1445 if value.trunc() != value {
1446 return Err(NavParseError::BadHeaderField { field });
1447 }
1448 Ok(value)
1449}
1450
1451fn strict_record_int<T>(
1452 line: &str,
1453 start: usize,
1454 end: usize,
1455 field: &'static str,
1456 satellite: &str,
1457) -> Result<T, NavParseError>
1458where
1459 T: core::str::FromStr,
1460{
1461 validate::strict_int::<T>(raw_field(line, start, end), field)
1462 .map_err(|error| map_record_field_error(error, satellite))
1463}
1464
1465fn map_record_field_error(error: FieldError, satellite: &str) -> NavParseError {
1466 NavParseError::BadField {
1467 satellite: satellite.to_string(),
1468 field: error.field(),
1469 }
1470}
1471
1472fn map_header_field_error(error: FieldError) -> NavParseError {
1473 NavParseError::BadHeaderField {
1474 field: error.field(),
1475 }
1476}
1477
1478fn parse_toc(
1481 l0: &str,
1482 sat: &str,
1483 time_scale: TimeScale,
1484) -> Result<ClockReferenceEpoch, NavParseError> {
1485 let year = strict_record_int::<i64>(l0, 4, 8, "toc epoch", sat)?;
1486 let month = strict_record_int::<i64>(l0, 9, 11, "toc epoch", sat)?;
1487 let day = strict_record_int::<i64>(l0, 12, 14, "toc epoch", sat)?;
1488 let hour = strict_record_int::<i64>(l0, 15, 17, "toc epoch", sat)?;
1489 let minute = strict_record_int::<i64>(l0, 18, 20, "toc epoch", sat)?;
1490 let second = strict_record_int::<i64>(l0, 21, 23, "toc epoch", sat)?;
1491 let civil = validate::civil_datetime_with_second_policy(
1492 year,
1493 month,
1494 day,
1495 hour,
1496 minute,
1497 second as f64,
1498 validate::CivilSecondPolicy::Continuous,
1499 )
1500 .map_err(|_| NavParseError::BadField {
1501 satellite: sat.to_string(),
1502 field: "toc epoch",
1503 })?;
1504 let month = i64::from(civil.month);
1505 let day = i64::from(civil.day);
1506 let week = gnss::week_from_calendar(time_scale, civil.year, month, day).ok_or_else(|| {
1507 NavParseError::BadField {
1508 satellite: sat.to_string(),
1509 field: "toc epoch",
1510 }
1511 })?;
1512 let sow = gnss::seconds_of_week_from_calendar(
1513 civil.year,
1514 month,
1515 day,
1516 i64::from(civil.hour),
1517 i64::from(civil.minute),
1518 civil.second as i64,
1519 );
1520 Ok(ClockReferenceEpoch { week, sow })
1521}
1522
1523#[cfg(all(test, sidereon_repo_tests))]
1524mod tests;