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;
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 * 3600.0;
63
64pub(crate) const GLONASS_MAX_AGE_S: f64 = 15.0 * 60.0;
68const GPS_NOMINAL_FIT_INTERVAL_S: f64 = 4.0 * 3600.0;
69const GPS_LEGACY_EXTENDED_FIT_INTERVAL_S: f64 = 8.0 * 3600.0;
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 enum BroadcastGroupDelayTerm {
107 GpsTgd,
109 GalileoBgdE5aE1,
111 GalileoBgdE5bE1,
113 BeidouTgd1,
115 BeidouTgd2,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Default)]
121pub struct BroadcastGroupDelays {
122 pub gps_tgd_s: Option<f64>,
124 pub galileo_bgd_e5a_e1_s: Option<f64>,
126 pub galileo_bgd_e5b_e1_s: Option<f64>,
128 pub beidou_tgd1_s: Option<f64>,
130 pub beidou_tgd2_s: Option<f64>,
132}
133
134impl BroadcastGroupDelays {
135 pub const fn gps_lnav(tgd_s: f64) -> Self {
137 Self {
138 gps_tgd_s: Some(tgd_s),
139 galileo_bgd_e5a_e1_s: None,
140 galileo_bgd_e5b_e1_s: None,
141 beidou_tgd1_s: None,
142 beidou_tgd2_s: None,
143 }
144 }
145
146 pub const fn galileo(bgd_e5a_e1_s: f64, bgd_e5b_e1_s: f64) -> Self {
148 Self {
149 gps_tgd_s: None,
150 galileo_bgd_e5a_e1_s: Some(bgd_e5a_e1_s),
151 galileo_bgd_e5b_e1_s: Some(bgd_e5b_e1_s),
152 beidou_tgd1_s: None,
153 beidou_tgd2_s: None,
154 }
155 }
156
157 pub const fn beidou(tgd1_s: f64, tgd2_s: f64) -> Self {
159 Self {
160 gps_tgd_s: None,
161 galileo_bgd_e5a_e1_s: None,
162 galileo_bgd_e5b_e1_s: None,
163 beidou_tgd1_s: Some(tgd1_s),
164 beidou_tgd2_s: Some(tgd2_s),
165 }
166 }
167
168 pub const fn get(&self, term: BroadcastGroupDelayTerm) -> Option<f64> {
170 match term {
171 BroadcastGroupDelayTerm::GpsTgd => self.gps_tgd_s,
172 BroadcastGroupDelayTerm::GalileoBgdE5aE1 => self.galileo_bgd_e5a_e1_s,
173 BroadcastGroupDelayTerm::GalileoBgdE5bE1 => self.galileo_bgd_e5b_e1_s,
174 BroadcastGroupDelayTerm::BeidouTgd1 => self.beidou_tgd1_s,
175 BroadcastGroupDelayTerm::BeidouTgd2 => self.beidou_tgd2_s,
176 }
177 }
178
179 pub const fn for_message(self, system: GnssSystem, message: NavMessage) -> Option<f64> {
184 match (system, message) {
185 (GnssSystem::Gps, NavMessage::GpsLnav) => self.get(BroadcastGroupDelayTerm::GpsTgd),
186 (GnssSystem::Galileo, NavMessage::GalileoFnav) => {
187 self.get(BroadcastGroupDelayTerm::GalileoBgdE5aE1)
188 }
189 (GnssSystem::Galileo, NavMessage::GalileoInav) => {
190 self.get(BroadcastGroupDelayTerm::GalileoBgdE5bE1)
191 }
192 (GnssSystem::BeiDou, NavMessage::BeidouD1 | NavMessage::BeidouD2) => {
193 self.get(BroadcastGroupDelayTerm::BeidouTgd1)
194 }
195 _ => None,
196 }
197 }
198}
199
200pub fn is_beidou_geo(sat: GnssSatelliteId) -> bool {
203 sat.system == GnssSystem::BeiDou && (sat.prn <= 5 || (59..=61).contains(&sat.prn))
204}
205
206#[derive(Debug, Clone, Copy, PartialEq)]
210pub struct KlobucharAlphaBeta {
211 pub alpha: [f64; 4],
213 pub beta: [f64; 4],
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Default)]
224pub struct IonoCorrections {
225 pub gps: Option<KlobucharAlphaBeta>,
227 pub beidou: Option<KlobucharAlphaBeta>,
229 pub galileo: Option<GalileoNequickCoeffs>,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
237pub struct GlonassRecord {
238 pub satellite_id: GnssSatelliteId,
240 pub toe_utc_j2000_s: f64,
243 pub pos_m: [f64; 3],
245 pub vel_m_s: [f64; 3],
247 pub acc_m_s2: [f64; 3],
249 pub clk_bias: f64,
251 pub gamma_n: f64,
253 pub sv_health: f64,
255 pub freq_channel: i32,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct SkippedGlonass {
264 pub token: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Default)]
276pub struct GlonassParse {
277 pub records: Vec<GlonassRecord>,
279 pub skipped: Vec<SkippedGlonass>,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq)]
285pub struct BroadcastRecord {
286 pub satellite_id: GnssSatelliteId,
288 pub message: NavMessage,
290 pub week: u32,
292 pub toe: GnssWeekTow,
294 pub toc: GnssWeekTow,
296 pub elements: KeplerianElements,
298 pub clock: ClockPolynomial,
300 pub group_delays: BroadcastGroupDelays,
302 pub sv_health: f64,
304 pub sv_accuracy_m: f64,
306 pub fit_interval_s: Option<f64>,
311}
312
313impl BroadcastRecord {
314 pub const fn time_scale(&self) -> TimeScale {
316 self.toe.system
317 }
318
319 pub const fn constants(&self) -> ConstellationConstants {
321 match self.satellite_id.system {
322 GnssSystem::Galileo => ConstellationConstants::GALILEO,
323 GnssSystem::BeiDou => ConstellationConstants::BEIDOU,
324 _ => ConstellationConstants::GPS,
326 }
327 }
328
329 pub fn broadcast_clock_group_delay_s(&self) -> f64 {
331 self.group_delays
332 .for_message(self.satellite_id.system, self.message)
333 .unwrap_or(0.0)
334 }
335
336 pub fn from_lnav(
370 decoded: &crate::navigation::lnav::LnavDecoded,
371 satellite_id: GnssSatelliteId,
372 full_week: u32,
373 ) -> Result<Self, LnavRecordError> {
374 if satellite_id.system != GnssSystem::Gps {
375 return Err(LnavRecordError::NotGps(satellite_id));
376 }
377
378 if i64::from(full_week % 1024) != decoded.week_number {
383 return Err(LnavRecordError::WeekMismatch {
384 full_week,
385 decoded_week: decoded.week_number,
386 });
387 }
388
389 let sv_accuracy_m = gps_ura_index_to_meters(decoded.ura_index)
390 .ok_or(LnavRecordError::NoUraPrediction(decoded.ura_index))?;
391 let fit_interval_s =
392 gps_fit_interval_from_flag(decoded.fit_interval_flag, decoded.iode, decoded.iodc)?;
393
394 const SEMICIRCLE_TO_RAD: f64 = core::f64::consts::PI;
397
398 let elements = KeplerianElements {
399 sqrt_a: decoded.sqrt_a,
400 e: decoded.eccentricity,
401 m0: decoded.m0 * SEMICIRCLE_TO_RAD,
402 delta_n: decoded.delta_n * SEMICIRCLE_TO_RAD,
403 omega0: decoded.omega0 * SEMICIRCLE_TO_RAD,
404 i0: decoded.i0 * SEMICIRCLE_TO_RAD,
405 omega: decoded.omega * SEMICIRCLE_TO_RAD,
406 omega_dot: decoded.omega_dot * SEMICIRCLE_TO_RAD,
407 idot: decoded.idot * SEMICIRCLE_TO_RAD,
408 cuc: decoded.cuc,
409 cus: decoded.cus,
410 crc: decoded.crc,
411 crs: decoded.crs,
412 cic: decoded.cic,
413 cis: decoded.cis,
414 toe_sow: decoded.toe as f64,
415 };
416 let clock = ClockPolynomial {
417 af0: decoded.af0,
418 af1: decoded.af1,
419 af2: decoded.af2,
420 toc_sow: decoded.toc as f64,
421 };
422
423 let toe = GnssWeekTow::new(TimeScale::Gpst, full_week, elements.toe_sow)
424 .and_then(GnssWeekTow::normalized)
425 .map_err(|_| LnavRecordError::InvalidEpoch("toe"))?;
426 let toc = GnssWeekTow::new(TimeScale::Gpst, full_week, clock.toc_sow)
427 .and_then(GnssWeekTow::normalized)
428 .map_err(|_| LnavRecordError::InvalidEpoch("toc"))?;
429
430 Ok(BroadcastRecord {
431 satellite_id,
432 message: NavMessage::GpsLnav,
433 week: full_week,
434 toe,
435 toc,
436 elements,
437 clock,
438 group_delays: BroadcastGroupDelays::gps_lnav(decoded.tgd),
439 sv_health: decoded.sv_health as f64,
440 sv_accuracy_m,
441 fit_interval_s: Some(fit_interval_s),
442 })
443 }
444}
445
446fn gps_ura_index_to_meters(index: i64) -> Option<f64> {
452 let meters = match index {
453 0 => 2.4,
454 1 => 3.4,
455 2 => 4.85,
456 3 => 6.85,
457 4 => 9.65,
458 5 => 13.65,
459 6 => 24.0,
460 7 => 48.0,
461 8 => 96.0,
462 9 => 192.0,
463 10 => 384.0,
464 11 => 768.0,
465 12 => 1536.0,
466 13 => 3072.0,
467 14 => 6144.0,
468 _ => return None,
471 };
472 Some(meters)
473}
474
475const GPS_FIT_INTERVAL_6H_S: f64 = 6.0 * 3600.0;
476const GPS_FIT_INTERVAL_8H_S: f64 = 8.0 * 3600.0;
477const GPS_FIT_INTERVAL_14H_S: f64 = 14.0 * 3600.0;
478const GPS_FIT_INTERVAL_26H_S: f64 = 26.0 * 3600.0;
479
480fn gps_fit_interval_from_flag(
490 fit_interval_flag: i64,
491 iode: i64,
492 iodc: i64,
493) -> Result<f64, LnavRecordError> {
494 let unsupported = || LnavRecordError::FitIntervalUnsupported {
495 fit_interval_flag,
496 iode,
497 iodc,
498 };
499 match fit_interval_flag {
500 0 => Ok(GPS_NOMINAL_FIT_INTERVAL_S),
501 1 => {
502 if (0..240).contains(&iode) {
503 Ok(GPS_FIT_INTERVAL_6H_S)
507 } else if (240..=255).contains(&iode) {
508 match iodc {
510 240..=247 => Ok(GPS_FIT_INTERVAL_8H_S),
511 248..=255 | 496 => Ok(GPS_FIT_INTERVAL_14H_S),
512 497..=503 | 1021..=1023 => Ok(GPS_FIT_INTERVAL_26H_S),
513 _ => Err(unsupported()),
514 }
515 } else {
516 Err(unsupported())
517 }
518 }
519 _ => Err(unsupported()),
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum LnavRecordError {
526 NotGps(GnssSatelliteId),
528 InvalidEpoch(&'static str),
530 WeekMismatch {
533 full_week: u32,
535 decoded_week: i64,
537 },
538 NoUraPrediction(i64),
540 FitIntervalUnsupported {
543 fit_interval_flag: i64,
545 iode: i64,
547 iodc: i64,
549 },
550}
551
552impl core::fmt::Display for LnavRecordError {
553 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
554 match self {
555 LnavRecordError::NotGps(sat) => {
556 write!(f, "LNAV is a GPS message; {sat} is not a GPS satellite")
557 }
558 LnavRecordError::InvalidEpoch(field) => {
559 write!(f, "derived {field} week/TOW is not representable")
560 }
561 LnavRecordError::WeekMismatch {
562 full_week,
563 decoded_week,
564 } => write!(
565 f,
566 "full_week {full_week} (week % 1024 = {}) disagrees with decoded 10-bit week {decoded_week}",
567 full_week % 1024
568 ),
569 LnavRecordError::NoUraPrediction(index) => {
570 write!(f, "URA index {index} carries no accuracy prediction")
571 }
572 LnavRecordError::FitIntervalUnsupported {
573 fit_interval_flag,
574 iode,
575 iodc,
576 } => write!(
577 f,
578 "fit interval flag {fit_interval_flag} with IODE {iode} / IODC {iodc} is not a defined curve-fit interval"
579 ),
580 }
581 }
582}
583
584impl std::error::Error for LnavRecordError {}
585
586fn broadcast_time_scale(system: GnssSystem) -> TimeScale {
587 match system {
588 GnssSystem::Galileo => TimeScale::Gst,
589 GnssSystem::BeiDou => TimeScale::Bdt,
590 _ => TimeScale::Gpst,
591 }
592}
593
594#[derive(Debug, Clone, PartialEq, Eq)]
596pub enum NavParseError {
597 UnsupportedHeader(String),
599 MissingHeaderEnd,
601 TruncatedRecord(String),
603 BadField {
605 satellite: String,
607 field: &'static str,
609 },
610 BadHeaderField {
612 field: &'static str,
614 },
615}
616
617impl core::fmt::Display for NavParseError {
618 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
619 match self {
620 NavParseError::UnsupportedHeader(s) => write!(f, "unsupported RINEX NAV header: {s}"),
621 NavParseError::MissingHeaderEnd => write!(f, "no END OF HEADER line"),
622 NavParseError::TruncatedRecord(s) => write!(f, "truncated navigation record for {s}"),
623 NavParseError::BadField { satellite, field } => {
624 write!(f, "bad/missing {field} field in record for {satellite}")
625 }
626 NavParseError::BadHeaderField { field } => {
627 write!(f, "bad/missing {field} field in navigation header")
628 }
629 }
630 }
631}
632
633impl std::error::Error for NavParseError {}
634
635pub fn parse_nav(text: &str) -> Result<Vec<BroadcastRecord>, NavParseError> {
645 let mut lines = text.lines();
646 let version = verify_and_skip_header(&mut lines)?;
647 if version.major >= 4 {
648 parse_nav_v4(lines, version)
649 } else {
650 parse_nav_v3(lines, version)
651 }
652}
653
654fn parse_nav_v3<'a, I>(
657 lines: I,
658 version: RinexVersion,
659) -> Result<Vec<BroadcastRecord>, NavParseError>
660where
661 I: Iterator<Item = &'a str>,
662{
663 let mut blocks: Vec<Vec<&str>> = Vec::new();
664 for line in lines {
665 if is_record_start(line) {
666 blocks.push(vec![line]);
667 } else if let Some(last) = blocks.last_mut() {
668 last.push(line);
669 }
670 }
671
672 let mut records = Vec::new();
673 for block in &blocks {
674 let letter = block[0].as_bytes()[0] as char;
675 match GnssSystem::from_letter(letter) {
676 Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou) => {
677 records.push(parse_keplerian_block(block, None, version)?);
678 }
679 _ => {}
681 }
682 }
683 Ok(records)
684}
685
686fn parse_nav_v4<'a, I>(
695 lines: I,
696 version: RinexVersion,
697) -> Result<Vec<BroadcastRecord>, NavParseError>
698where
699 I: Iterator<Item = &'a str>,
700{
701 let frames = v4_frames(lines);
704 let mut records = Vec::new();
705 for (marker, body) in &frames {
706 let Some((frame_type, sv, msg_token)) = parse_v4_marker(marker) else {
707 continue;
708 };
709 if frame_type != "EPH" {
710 continue; }
712 let letter = sv.as_bytes().first().copied().map_or(' ', char::from);
713 let supported = matches!(
714 GnssSystem::from_letter(letter),
715 Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou)
716 );
717 if !supported {
718 continue; }
720 if let Some(message) = nav_message_from_v4_token(msg_token) {
723 validate_v4_ephemeris_marker(sv, message, body)?;
724 records.push(parse_keplerian_block(body, Some(message), version)?);
725 }
726 }
727 Ok(records)
728}
729
730fn v4_frames<'a, I>(lines: I) -> Vec<(&'a str, Vec<&'a str>)>
731where
732 I: Iterator<Item = &'a str>,
733{
734 let mut frames: Vec<(&str, Vec<&str>)> = Vec::new();
735 for line in lines {
736 if is_v4_frame_marker(line) {
737 frames.push((line, Vec::new()));
738 } else if let Some((_, body)) = frames.last_mut() {
739 body.push(line);
740 }
741 }
742 frames
743}
744
745fn is_v4_frame_marker(line: &str) -> bool {
747 line.starts_with("> ")
748}
749
750fn parse_v4_marker(line: &str) -> Option<(&str, &str, &str)> {
754 let rest = line.strip_prefix('>')?;
755 let mut fields = rest.split_whitespace();
756 let frame_type = fields.next()?;
757 let sv = fields.next()?;
758 let msg_token = fields.next()?;
759 Some((frame_type, sv, msg_token))
760}
761
762fn nav_message_from_v4_token(token: &str) -> Option<NavMessage> {
766 match token {
767 "LNAV" => Some(NavMessage::GpsLnav),
768 "INAV" => Some(NavMessage::GalileoInav),
769 "FNAV" => Some(NavMessage::GalileoFnav),
770 "D1" => Some(NavMessage::BeidouD1),
771 "D2" => Some(NavMessage::BeidouD2),
772 _ => None,
773 }
774}
775
776fn validate_v4_ephemeris_marker(
777 marker_sv: &str,
778 message: NavMessage,
779 body: &[&str],
780) -> Result<(), NavParseError> {
781 let Some(body_sv) = body
782 .first()
783 .and_then(|line| line.get(0..3))
784 .map(str::trim)
785 .filter(|sv| !sv.is_empty())
786 else {
787 return Ok(());
788 };
789
790 if marker_sv != body_sv {
791 return Err(NavParseError::BadField {
792 satellite: marker_sv.to_string(),
793 field: "frame marker",
794 });
795 }
796
797 let system = body_sv
798 .as_bytes()
799 .first()
800 .and_then(|b| GnssSystem::from_letter(*b as char))
801 .ok_or_else(|| NavParseError::BadField {
802 satellite: body_sv.to_string(),
803 field: "system",
804 })?;
805 if !nav_message_matches_system(message, system) {
806 return Err(NavParseError::BadField {
807 satellite: body_sv.to_string(),
808 field: "message",
809 });
810 }
811
812 Ok(())
813}
814
815fn nav_message_matches_system(message: NavMessage, system: GnssSystem) -> bool {
816 matches!(
817 (message, system),
818 (NavMessage::GpsLnav, GnssSystem::Gps)
819 | (
820 NavMessage::GalileoInav | NavMessage::GalileoFnav,
821 GnssSystem::Galileo,
822 )
823 | (
824 NavMessage::BeidouD1 | NavMessage::BeidouD2,
825 GnssSystem::BeiDou,
826 )
827 )
828}
829
830pub fn parse_iono_corrections(text: &str) -> Result<IonoCorrections, NavParseError> {
838 parse_iono_corrections_checked(text)
839}
840
841fn parse_iono_corrections_checked(text: &str) -> Result<IonoCorrections, NavParseError> {
842 let klobuchar_row = |line: &str| -> Result<[f64; 4], NavParseError> {
849 Ok([
850 strict_header_f64(line, 5, 17, "ionospheric correction")?,
851 strict_header_f64(line, 17, 29, "ionospheric correction")?,
852 strict_header_f64(line, 29, 41, "ionospheric correction")?,
853 strict_header_f64(line, 41, 53, "ionospheric correction")?,
854 ])
855 };
856 let nequick_row = |line: &str| -> Result<[f64; 3], NavParseError> {
861 Ok([
862 strict_header_f64(line, 5, 17, "ionospheric correction")?,
863 strict_header_f64(line, 17, 29, "ionospheric correction")?,
864 strict_header_f64(line, 29, 41, "ionospheric correction")?,
865 ])
866 };
867 let (mut gpsa, mut gpsb, mut bdsa, mut bdsb, mut gal) = (None, None, None, None, None);
868 for line in text.lines() {
869 if line.contains("END OF HEADER") {
870 break;
871 }
872 if !line.contains("IONOSPHERIC CORR") {
873 continue;
874 }
875 match line.get(0..4).map(str::trim) {
876 Some("GPSA") => gpsa = Some(klobuchar_row(line)?),
877 Some("GPSB") => gpsb = Some(klobuchar_row(line)?),
878 Some("BDSA") => bdsa = Some(klobuchar_row(line)?),
879 Some("BDSB") => bdsb = Some(klobuchar_row(line)?),
880 Some("GAL") => {
881 let row = nequick_row(line)?;
882 gal = Some(GalileoNequickCoeffs {
883 ai0: row[0],
884 ai1: row[1],
885 ai2: row[2],
886 });
887 }
888 _ => {}
889 }
890 }
891 let pair = |a: Option<[f64; 4]>, b: Option<[f64; 4]>| match (a, b) {
892 (Some(alpha), Some(beta)) => Some(KlobucharAlphaBeta { alpha, beta }),
893 _ => None,
894 };
895 let mut iono = IonoCorrections {
896 gps: pair(gpsa, gpsb),
897 beidou: pair(bdsa, bdsb),
898 galileo: gal,
899 };
900 parse_v4_body_iono_corrections(text, &mut iono)?;
901 Ok(iono)
902}
903
904fn parse_v4_body_iono_corrections(
905 text: &str,
906 iono: &mut IonoCorrections,
907) -> Result<(), NavParseError> {
908 let mut lines = text.lines();
909 for line in lines.by_ref() {
910 if line.contains("END OF HEADER") {
911 break;
912 }
913 }
914
915 for (marker, body) in v4_frames(lines) {
916 let Some((frame_type, sv, _msg_token)) = parse_v4_marker(marker) else {
917 continue;
918 };
919 if frame_type != "ION" {
920 continue;
921 }
922 let values = parse_v4_iono_values(sv, &body)?;
923 match sv
924 .as_bytes()
925 .first()
926 .and_then(|b| GnssSystem::from_letter(*b as char))
927 {
928 Some(GnssSystem::Gps) => {
929 iono.gps = Some(KlobucharAlphaBeta {
930 alpha: iono_values_4(&values, 0, sv)?,
931 beta: iono_values_4(&values, 4, sv)?,
932 });
933 }
934 Some(GnssSystem::BeiDou) => {
935 iono.beidou = Some(KlobucharAlphaBeta {
936 alpha: iono_values_4(&values, 0, sv)?,
937 beta: iono_values_4(&values, 4, sv)?,
938 });
939 }
940 Some(GnssSystem::Galileo) => {
941 let coeffs = iono_values_3(&values, 0, sv)?;
942 iono.galileo = Some(GalileoNequickCoeffs {
943 ai0: coeffs[0],
944 ai1: coeffs[1],
945 ai2: coeffs[2],
946 });
947 }
948 _ => {}
949 }
950 }
951 Ok(())
952}
953
954fn parse_v4_iono_values(sv: &str, body: &[&str]) -> Result<Vec<f64>, NavParseError> {
955 if body.is_empty() {
956 return Err(NavParseError::BadField {
957 satellite: sv.to_string(),
958 field: "ionospheric correction",
959 });
960 }
961
962 let mut values = Vec::new();
963 for (idx, line) in body.iter().enumerate() {
964 let ranges: &[(usize, usize)] = if idx == 0 {
965 &[(23, 42), (42, 61), (61, 80)]
966 } else {
967 &[(4, 23), (23, 42), (42, 61), (61, 80)]
968 };
969 for &(start, end) in ranges {
970 let raw = raw_field(line, start, end);
971 if raw.trim().is_empty() {
972 continue;
973 }
974 values.push(
975 validate::strict_f64(raw, "ionospheric correction")
976 .map_err(|error| map_record_field_error(error, sv))?,
977 );
978 }
979 }
980 Ok(values)
981}
982
983fn iono_values_4(values: &[f64], start: usize, sv: &str) -> Result<[f64; 4], NavParseError> {
984 let Some(slice) = values.get(start..start + 4) else {
985 return Err(NavParseError::BadField {
986 satellite: sv.to_string(),
987 field: "ionospheric correction",
988 });
989 };
990 Ok([slice[0], slice[1], slice[2], slice[3]])
991}
992
993fn iono_values_3(values: &[f64], start: usize, sv: &str) -> Result<[f64; 3], NavParseError> {
994 let Some(slice) = values.get(start..start + 3) else {
995 return Err(NavParseError::BadField {
996 satellite: sv.to_string(),
997 field: "ionospheric correction",
998 });
999 };
1000 Ok([slice[0], slice[1], slice[2]])
1001}
1002
1003pub fn parse_leap_seconds(text: &str) -> Result<Option<f64>, NavParseError> {
1007 parse_leap_seconds_checked(text)
1008}
1009
1010fn parse_leap_seconds_checked(text: &str) -> Result<Option<f64>, NavParseError> {
1011 for line in text.lines() {
1012 if line.contains("END OF HEADER") {
1013 break;
1014 }
1015 if line.contains("LEAP SECONDS") {
1016 return strict_header_integer_f64(line, 0, 6, "leap seconds").map(Some);
1017 }
1018 }
1019 Ok(None)
1020}
1021
1022fn j2000_seconds_utc(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> f64 {
1027 civil::j2000_seconds(y as i32, mo as i32, d as i32, h as i32, mi as i32, s as f64)
1028}
1029
1030fn parse_glonass_epoch(l0: &str, sat: &str) -> Result<f64, NavParseError> {
1033 let year = strict_record_int::<i64>(l0, 4, 8, "epoch", sat)?;
1034 let month = strict_record_int::<i64>(l0, 9, 11, "epoch", sat)?;
1035 let day = strict_record_int::<i64>(l0, 12, 14, "epoch", sat)?;
1036 let hour = strict_record_int::<i64>(l0, 15, 17, "epoch", sat)?;
1037 let minute = strict_record_int::<i64>(l0, 18, 20, "epoch", sat)?;
1038 let second = strict_record_int::<i64>(l0, 21, 23, "epoch", sat)?;
1039 let civil = validate::civil_datetime_with_second_policy(
1040 year,
1041 month,
1042 day,
1043 hour,
1044 minute,
1045 second as f64,
1046 validate::CivilSecondPolicy::UtcLike,
1047 )
1048 .map_err(|_| NavParseError::BadField {
1049 satellite: sat.to_string(),
1050 field: "epoch",
1051 })?;
1052 Ok(j2000_seconds_utc(
1053 civil.year,
1054 i64::from(civil.month),
1055 i64::from(civil.day),
1056 i64::from(civil.hour),
1057 i64::from(civil.minute),
1058 civil.second as i64,
1059 ))
1060}
1061
1062fn parse_glonass_block(block: &[&str]) -> Result<GlonassRecord, NavParseError> {
1066 let l0 = block[0];
1067 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1068 if block.len() < 4 {
1069 return Err(NavParseError::TruncatedRecord(sat));
1070 }
1071 let bad = |what: &'static str| NavParseError::BadField {
1072 satellite: sat.clone(),
1073 field: what,
1074 };
1075 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1076 let toe_utc_j2000_s = parse_glonass_epoch(l0, &sat)?;
1077 let clk_bias = parse_f64(l0, 23, 42).ok_or_else(|| bad("clock bias"))?;
1078 let gamma_n = parse_f64(l0, 42, 61).ok_or_else(|| bad("gamma_n"))?;
1079 let o1 = orbit_row(block[1]);
1080 let o2 = orbit_row(block[2]);
1081 let o3 = orbit_row(block[3]);
1082 let km = |v: Option<f64>, what: &'static str| v.map(|x| x * KM_TO_M).ok_or_else(|| bad(what));
1083 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1084 Ok(GlonassRecord {
1085 satellite_id,
1086 toe_utc_j2000_s,
1087 pos_m: [km(o1[0], "x")?, km(o2[0], "y")?, km(o3[0], "z")?],
1088 vel_m_s: [km(o1[1], "vx")?, km(o2[1], "vy")?, km(o3[1], "vz")?],
1089 acc_m_s2: [km(o1[2], "ax")?, km(o2[2], "ay")?, km(o3[2], "az")?],
1090 clk_bias,
1091 gamma_n,
1092 sv_health: g(o1[3], "health")?,
1093 freq_channel: glonass_frequency_channel(g(o2[3], "frequency channel")?, &sat)?,
1094 })
1095}
1096
1097pub fn parse_glonass(text: &str) -> Result<Vec<GlonassRecord>, NavParseError> {
1105 Ok(parse_glonass_lenient(text)?.records)
1106}
1107
1108pub fn parse_glonass_lenient(text: &str) -> Result<GlonassParse, NavParseError> {
1117 let mut lines = text.lines();
1118 verify_and_skip_header(&mut lines)?;
1119 let mut blocks: Vec<Vec<&str>> = Vec::new();
1120 for line in lines {
1121 if is_record_start(line) {
1122 blocks.push(vec![line]);
1123 } else if let Some(last) = blocks.last_mut() {
1124 last.push(line);
1125 }
1126 }
1127 let mut out = GlonassParse::default();
1128 for block in blocks.iter().filter(|b| b[0].starts_with('R')) {
1129 let sat = block[0].get(0..3).unwrap_or("").trim();
1135 if sat.parse::<GnssSatelliteId>().is_err() {
1136 out.skipped.push(SkippedGlonass {
1137 token: sat.to_string(),
1138 });
1139 continue;
1140 }
1141 out.records.push(parse_glonass_block(block)?);
1142 }
1143 Ok(out)
1144}
1145
1146fn verify_and_skip_header<'a, I>(lines: &mut I) -> Result<RinexVersion, NavParseError>
1150where
1151 I: Iterator<Item = &'a str>,
1152{
1153 let mut version_seen: Option<RinexVersion> = None;
1154 for line in lines.by_ref() {
1155 if line.contains("RINEX VERSION / TYPE") {
1156 let version = line.get(0..9).unwrap_or("").trim();
1158 let detected = parse_rinex_version(version);
1159 let is_nav = line.get(20..21) == Some("N");
1160 match (detected, is_nav) {
1161 (Some(v), true) => version_seen = Some(v),
1162 _ => {
1163 return Err(NavParseError::UnsupportedHeader(
1164 line.trim_end().to_string(),
1165 ))
1166 }
1167 }
1168 }
1169 if line.contains("END OF HEADER") {
1170 return version_seen.ok_or_else(|| {
1171 NavParseError::UnsupportedHeader("no RINEX VERSION / TYPE".to_string())
1172 });
1173 }
1174 }
1175 Err(NavParseError::MissingHeaderEnd)
1176}
1177
1178fn parse_rinex_version(version: &str) -> Option<RinexVersion> {
1179 let (major, minor) = version.split_once('.')?;
1180 let major = major.trim().parse::<u8>().ok()?;
1181 if !matches!(major, 3 | 4) {
1182 return None;
1183 }
1184 let minor_digits = minor
1185 .chars()
1186 .take_while(char::is_ascii_digit)
1187 .collect::<String>();
1188 if minor_digits.is_empty() {
1189 return None;
1190 }
1191 let minor = minor_digits.parse::<u8>().ok()?;
1192 Some(RinexVersion { major, minor })
1193}
1194
1195fn is_record_start(line: &str) -> bool {
1196 let b = line.as_bytes();
1197 b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1].is_ascii_digit() && b[2].is_ascii_digit()
1198}
1199
1200fn orbit_row(line: &str) -> [Option<f64>; 4] {
1202 [
1203 parse_f64(line, 4, 23),
1204 parse_f64(line, 23, 42),
1205 parse_f64(line, 42, 61),
1206 parse_f64(line, 61, 80),
1207 ]
1208}
1209
1210#[derive(Debug, Clone, Copy)]
1211struct ClockReferenceEpoch {
1212 week: u32,
1213 sow: f64,
1214}
1215
1216fn parse_keplerian_block(
1217 block: &[&str],
1218 message_override: Option<NavMessage>,
1219 version: RinexVersion,
1220) -> Result<BroadcastRecord, NavParseError> {
1221 let l0 = block.first().copied().unwrap_or("");
1222 let sat = l0.get(0..3).unwrap_or("").trim().to_string();
1223 if block.len() < 8 {
1224 return Err(NavParseError::TruncatedRecord(sat));
1225 }
1226 let bad = |what: &'static str| NavParseError::BadField {
1227 satellite: sat.clone(),
1228 field: what,
1229 };
1230
1231 let letter = l0
1232 .as_bytes()
1233 .first()
1234 .copied()
1235 .map(|b| b as char)
1236 .ok_or_else(|| bad("system"))?;
1237 let system = GnssSystem::from_letter(letter).ok_or_else(|| bad("system"))?;
1238 let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
1239
1240 let time_scale = broadcast_time_scale(system);
1242 let toc_epoch = parse_toc(l0, &sat, time_scale)?;
1243 let toc_sow = toc_epoch.sow;
1244 let af0 = parse_f64(l0, 23, 42).ok_or_else(|| bad("af0"))?;
1245 let af1 = parse_f64(l0, 42, 61).ok_or_else(|| bad("af1"))?;
1246 let af2 = parse_f64(l0, 61, 80).ok_or_else(|| bad("af2"))?;
1247
1248 let o1 = orbit_row(block[1]);
1249 let o2 = orbit_row(block[2]);
1250 let o3 = orbit_row(block[3]);
1251 let o4 = orbit_row(block[4]);
1252 let o5 = orbit_row(block[5]);
1253 let o6 = orbit_row(block[6]);
1254
1255 let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
1256
1257 let elements = KeplerianElements {
1258 crs: g(o1[1], "crs")?,
1259 delta_n: g(o1[2], "deltaN")?,
1260 m0: g(o1[3], "m0")?,
1261 cuc: g(o2[0], "cuc")?,
1262 e: g(o2[1], "e")?,
1263 cus: g(o2[2], "cus")?,
1264 sqrt_a: g(o2[3], "sqrtA")?,
1265 toe_sow: g(o3[0], "toe")?,
1266 cic: g(o3[1], "cic")?,
1267 omega0: g(o3[2], "omega0")?,
1268 cis: g(o3[3], "cis")?,
1269 i0: g(o4[0], "i0")?,
1270 crc: g(o4[1], "crc")?,
1271 omega: g(o4[2], "omega")?,
1272 omega_dot: g(o4[3], "omegaDot")?,
1273 idot: g(o5[0], "idot")?,
1274 };
1275 let clock = ClockPolynomial {
1276 af0,
1277 af1,
1278 af2,
1279 toc_sow,
1280 };
1281
1282 let week = finite_integral_u32(g(o5[2], "week")?, "week", &sat)?;
1283 let toe = GnssWeekTow::new(time_scale, week, elements.toe_sow)
1284 .and_then(GnssWeekTow::normalized)
1285 .map_err(|_| bad("toe"))?;
1286 let toc = GnssWeekTow::new(time_scale, toc_epoch.week, clock.toc_sow)
1287 .and_then(GnssWeekTow::normalized)
1288 .map_err(|_| bad("toc"))?;
1289 let message = if let Some(message) = message_override {
1290 message
1291 } else {
1292 match system {
1293 GnssSystem::Galileo => galileo_message(g(o5[1], "data sources")?, &sat)?,
1294 GnssSystem::BeiDou => {
1295 if is_beidou_geo(satellite_id) {
1296 NavMessage::BeidouD2
1297 } else {
1298 NavMessage::BeidouD1
1299 }
1300 }
1301 _ => NavMessage::GpsLnav,
1302 }
1303 };
1304
1305 let sv_accuracy_m = g(o6[0], "accuracy")?;
1306 let sv_health = g(o6[1], "health")?;
1307 let group_delays = match system {
1308 GnssSystem::Gps => BroadcastGroupDelays::gps_lnav(g(o6[2], "gps tgd")?),
1309 GnssSystem::Galileo => {
1313 BroadcastGroupDelays::galileo(g(o6[2], "bgd e5a/e1")?, g(o6[3], "bgd e5b/e1")?)
1314 }
1315 GnssSystem::BeiDou => {
1316 BroadcastGroupDelays::beidou(g(o6[2], "beidou tgd1")?, g(o6[3], "beidou tgd2")?)
1317 }
1318 _ => BroadcastGroupDelays::default(),
1319 };
1320
1321 let fit_interval_s = match system {
1324 GnssSystem::Gps => {
1325 Some(gps_fit_interval_s(block[7], version).map_err(|()| bad("fit interval"))?)
1326 }
1327 _ => None,
1328 };
1329
1330 Ok(BroadcastRecord {
1331 satellite_id,
1332 message,
1333 week,
1334 toe,
1335 toc,
1336 elements,
1337 clock,
1338 group_delays,
1339 sv_health,
1340 sv_accuracy_m,
1341 fit_interval_s,
1342 })
1343}
1344
1345fn gps_fit_interval_s(orbit7: &str, version: RinexVersion) -> Result<f64, ()> {
1357 let value = match field(orbit7, 23, 42) {
1358 None => 0.0,
1359 Some(_) => parse_f64(orbit7, 23, 42).ok_or(())?,
1360 };
1361 if value == 0.0 {
1362 Ok(GPS_NOMINAL_FIT_INTERVAL_S)
1363 } else if version.gps_fit_interval_uses_legacy_flag() && value == 1.0 {
1364 Ok(GPS_LEGACY_EXTENDED_FIT_INTERVAL_S)
1365 } else {
1366 Ok(value * 3600.0)
1367 }
1368}
1369
1370fn galileo_message(data_sources: f64, sat: &str) -> Result<NavMessage, NavParseError> {
1374 let word = finite_integral_u32(data_sources, "data sources", sat)?;
1375 if word & 0b010 != 0 {
1376 Ok(NavMessage::GalileoFnav)
1377 } else if word & 0b101 != 0 {
1378 Ok(NavMessage::GalileoInav)
1379 } else {
1380 Ok(NavMessage::GalileoInav)
1382 }
1383}
1384
1385fn finite_integral_u32(value: f64, field: &'static str, sat: &str) -> Result<u32, NavParseError> {
1386 validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
1387 if value < 0.0 || value > f64::from(u32::MAX) || value.trunc() != value {
1388 return Err(NavParseError::BadField {
1389 satellite: sat.to_string(),
1390 field,
1391 });
1392 }
1393 Ok(value as u32)
1394}
1395
1396fn glonass_frequency_channel(value: f64, sat: &str) -> Result<i32, NavParseError> {
1397 const FIELD: &str = "frequency channel";
1398 validate::finite(value, FIELD).map_err(|error| map_record_field_error(error, sat))?;
1399 let channel = value as i32;
1400 if value.trunc() != value || !valid_glonass_frequency_channel(channel) {
1401 return Err(NavParseError::BadField {
1402 satellite: sat.to_string(),
1403 field: FIELD,
1404 });
1405 }
1406 Ok(channel)
1407}
1408
1409fn strict_header_f64(
1410 line: &str,
1411 start: usize,
1412 end: usize,
1413 field: &'static str,
1414) -> Result<f64, NavParseError> {
1415 validate::strict_f64(raw_field(line, start, end), field).map_err(map_header_field_error)
1416}
1417
1418fn strict_header_integer_f64(
1419 line: &str,
1420 start: usize,
1421 end: usize,
1422 field: &'static str,
1423) -> Result<f64, NavParseError> {
1424 let value = strict_header_f64(line, start, end, field)?;
1425 if value.trunc() != value {
1426 return Err(NavParseError::BadHeaderField { field });
1427 }
1428 Ok(value)
1429}
1430
1431fn strict_record_int<T>(
1432 line: &str,
1433 start: usize,
1434 end: usize,
1435 field: &'static str,
1436 satellite: &str,
1437) -> Result<T, NavParseError>
1438where
1439 T: core::str::FromStr,
1440{
1441 validate::strict_int::<T>(raw_field(line, start, end), field)
1442 .map_err(|error| map_record_field_error(error, satellite))
1443}
1444
1445fn map_record_field_error(error: FieldError, satellite: &str) -> NavParseError {
1446 NavParseError::BadField {
1447 satellite: satellite.to_string(),
1448 field: error.field(),
1449 }
1450}
1451
1452fn map_header_field_error(error: FieldError) -> NavParseError {
1453 NavParseError::BadHeaderField {
1454 field: error.field(),
1455 }
1456}
1457
1458fn parse_toc(
1461 l0: &str,
1462 sat: &str,
1463 time_scale: TimeScale,
1464) -> Result<ClockReferenceEpoch, NavParseError> {
1465 let year = strict_record_int::<i64>(l0, 4, 8, "toc epoch", sat)?;
1466 let month = strict_record_int::<i64>(l0, 9, 11, "toc epoch", sat)?;
1467 let day = strict_record_int::<i64>(l0, 12, 14, "toc epoch", sat)?;
1468 let hour = strict_record_int::<i64>(l0, 15, 17, "toc epoch", sat)?;
1469 let minute = strict_record_int::<i64>(l0, 18, 20, "toc epoch", sat)?;
1470 let second = strict_record_int::<i64>(l0, 21, 23, "toc epoch", sat)?;
1471 let civil = validate::civil_datetime_with_second_policy(
1472 year,
1473 month,
1474 day,
1475 hour,
1476 minute,
1477 second as f64,
1478 validate::CivilSecondPolicy::Continuous,
1479 )
1480 .map_err(|_| NavParseError::BadField {
1481 satellite: sat.to_string(),
1482 field: "toc epoch",
1483 })?;
1484 let month = i64::from(civil.month);
1485 let day = i64::from(civil.day);
1486 let week = gnss::week_from_calendar(time_scale, civil.year, month, day).ok_or_else(|| {
1487 NavParseError::BadField {
1488 satellite: sat.to_string(),
1489 field: "toc epoch",
1490 }
1491 })?;
1492 let sow = gnss::seconds_of_week_from_calendar(
1493 civil.year,
1494 month,
1495 day,
1496 i64::from(civil.hour),
1497 i64::from(civil.minute),
1498 civil.second as i64,
1499 );
1500 Ok(ClockReferenceEpoch { week, sow })
1501}
1502
1503#[cfg(all(test, sidereon_repo_tests))]
1504mod tests;