1use std::borrow::Cow;
51use std::collections::BTreeMap;
52
53use crate::astro::time::model::TimeScale;
54
55use crate::format::columns::{raw_field as field, raw_field_from};
56use crate::format::{Diagnostics, RecordRef, Skip, SkipReason};
57use crate::frequencies::{
58 rinex_band_frequency_hz, rinex_observation_frequency_hz, rinex_observation_wavelength_m,
59};
60use crate::id::{GnssSatelliteId, GnssSystem};
61use crate::rinex_common::time_scale_label;
62use crate::rinex_nav::valid_glonass_frequency_channel;
63use crate::validate::{self, FieldError};
64use crate::{Error, Result};
65
66const OBS_FIELD_WIDTH: usize = 16;
68const OBS_VALUE_WIDTH: usize = 14;
70const MAX_EPOCH_RECORD_COUNT: usize = 999;
72const OBS_CODE_FIELD_WIDTH: usize = 3;
77const MAX_OBS_TYPE_COUNT: usize = 999;
81const HEADER_LABELS: &[&str] = &[
82 "RINEX VERSION / TYPE",
83 "PGM / RUN BY / DATE",
84 "COMMENT",
85 "APPROX POSITION XYZ",
86 "ANTENNA: DELTA H/E/N",
87 "SYS / # / OBS TYPES",
88 "# / TYPES OF OBSERV",
89 "SYS / SCALE FACTOR",
90 "SYS / PHASE SHIFT",
91 "TIME OF FIRST OBS",
92 "TIME OF LAST OBS",
93 "INTERVAL",
94 "GLONASS SLOT / FRQ #",
95 "GLONASS COD/PHS/BIS",
96 "SIGNAL STRENGTH UNIT",
97 "LEAP SECONDS",
98 "# OF SATELLITES",
99 "PRN / # OF OBS",
100 "MARKER NAME",
101 "MARKER NUMBER",
102 "MARKER TYPE",
103 "OBSERVER / AGENCY",
104 "REC # / TYPE / VERS",
105 "ANT # / TYPE",
106 "END OF HEADER",
107];
108
109#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
114pub struct ObsEpochTime {
115 pub year: i32,
117 pub month: u8,
119 pub day: u8,
121 pub hour: u8,
123 pub minute: u8,
125 pub second: f64,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq)]
132pub struct ObsValue {
133 pub value: Option<f64>,
136 pub lli: Option<u8>,
138 pub ssi: Option<u8>,
140}
141
142#[derive(Debug, Clone, PartialEq)]
144pub struct ObsPhaseShift {
145 pub system: GnssSystem,
147 pub code: String,
149 pub correction_cycles: f64,
151 pub satellites: Vec<GnssSatelliteId>,
154}
155
156#[derive(Debug, Clone, PartialEq)]
158pub struct ObsScaleFactor {
159 pub system: GnssSystem,
161 pub factor: f64,
163 pub codes: Vec<String>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct PgmRunByDate {
170 pub program: String,
172 pub run_by: String,
174 pub date: String,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ReceiverInfo {
181 pub number: String,
183 pub receiver_type: String,
185 pub version: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct AntennaInfo {
192 pub number: String,
194 pub antenna_type: String,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct ObsLeapSeconds {
201 pub current: i64,
203 pub delta_future: Option<i64>,
205 pub week: Option<i64>,
207 pub day: Option<i64>,
209}
210
211#[derive(Debug, Clone, PartialEq)]
214pub struct ObsEpoch {
215 pub epoch: ObsEpochTime,
217 pub flag: u8,
219 pub rcv_clock_offset_s: Option<f64>,
221 pub epoch_picoseconds: Option<u32>,
223 pub declared_record_count: usize,
225 pub special_record_count: usize,
227 pub sats: BTreeMap<GnssSatelliteId, Vec<ObsValue>>,
230}
231
232#[derive(Debug, Clone, PartialEq)]
234pub struct ObsHeader {
235 pub version: f64,
237 pub approx_position_m: Option<[f64; 3]>,
240 pub antenna_delta_hen_m: Option<[f64; 3]>,
244 pub obs_codes: BTreeMap<GnssSystem, Vec<String>>,
246 pub program_run_by_date: Option<PgmRunByDate>,
248 pub comments: Vec<String>,
250 pub marker_number: Option<String>,
252 pub marker_type: Option<String>,
254 pub observer: Option<String>,
256 pub agency: Option<String>,
258 pub receiver: Option<ReceiverInfo>,
260 pub antenna: Option<AntennaInfo>,
262 pub interval_s: Option<f64>,
267 pub time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
269 pub time_of_last_obs: Option<(ObsEpochTime, TimeScale)>,
271 pub n_satellites: Option<usize>,
273 pub prn_obs_counts: BTreeMap<GnssSatelliteId, Vec<Option<usize>>>,
275 pub phase_shifts: Vec<ObsPhaseShift>,
277 pub scale_factors: Vec<ObsScaleFactor>,
279 pub glonass_slots: BTreeMap<u8, i8>,
281 pub glonass_cod_phs_bis: Option<Vec<(String, f64)>>,
283 pub signal_strength_unit: Option<String>,
285 pub leap_seconds: Option<ObsLeapSeconds>,
287 pub marker_name: Option<String>,
289 pub unretained_header_labels: Vec<String>,
291}
292
293#[derive(Debug, Clone, PartialEq)]
299pub struct RinexObs {
300 pub header: ObsHeader,
302 pub epochs: Vec<ObsEpoch>,
305 pub skipped_records: usize,
312}
313
314impl RinexObs {
315 pub fn parse(text: &str) -> Result<Self> {
321 let mut parser = Parser::new();
322 let mut lines = text.lines();
323 parser.parse_header(&mut lines)?;
324 let mut body = lines.peekable();
325 if parser.is_rinex2() {
326 parser.parse_body_v2(&mut body)?;
327 } else {
328 parser.parse_body(&mut body)?;
329 }
330 parser.finish()
331 }
332
333 pub fn header(&self) -> &ObsHeader {
335 &self.header
336 }
337
338 pub fn epochs(&self) -> &[ObsEpoch] {
340 &self.epochs
341 }
342
343 pub fn obs_codes(&self, sys: GnssSystem) -> Option<&[String]> {
345 self.header.obs_codes.get(&sys).map(Vec::as_slice)
346 }
347}
348
349impl core::str::FromStr for RinexObs {
350 type Err = Error;
351
352 fn from_str(s: &str) -> Result<Self> {
353 Self::parse(s)
354 }
355}
356
357#[derive(Debug, Clone, PartialEq)]
364pub struct SignalPolicy {
365 pub codes: BTreeMap<GnssSystem, Vec<String>>,
367}
368
369impl SignalPolicy {
370 pub fn default_for(version: f64) -> Result<Self> {
380 validate_finite_input(version, "version")?;
381 let mut codes = BTreeMap::new();
382 codes.insert(GnssSystem::Gps, vec!["C1C".to_string()]);
383 codes.insert(
384 GnssSystem::Galileo,
385 vec!["C1C".to_string(), "C1X".to_string()],
386 );
387 let beidou = if (3.015..3.025).contains(&version) {
392 vec!["C1I".to_string(), "C2I".to_string()]
393 } else {
394 vec!["C2I".to_string(), "C1I".to_string()]
395 };
396 codes.insert(GnssSystem::BeiDou, beidou);
397 codes.insert(GnssSystem::Glonass, vec!["C1C".to_string()]);
398 Ok(Self { codes })
399 }
400
401 pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
403 self.codes.insert(sys, codes);
404 self
405 }
406}
407
408#[derive(Debug, Clone, Default, PartialEq, Eq)]
414pub struct ObservationFilter {
415 pub codes: BTreeMap<GnssSystem, Vec<String>>,
417}
418
419impl ObservationFilter {
420 pub fn all() -> Self {
422 Self::default()
423 }
424
425 pub fn from_entries<I>(entries: I) -> Self
427 where
428 I: IntoIterator<Item = (GnssSystem, Vec<String>)>,
429 {
430 Self {
431 codes: entries.into_iter().collect(),
432 }
433 }
434
435 fn allowed_codes(&self, system: GnssSystem) -> Option<&[String]> {
436 if self.codes.is_empty() {
437 Some(&[])
438 } else {
439 self.codes.get(&system).map(Vec::as_slice)
440 }
441 }
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub enum ObservationKind {
447 Pseudorange,
449 CarrierPhase,
451 Doppler,
453 SignalStrength,
455 Unknown,
457}
458
459impl ObservationKind {
460 pub fn from_code(code: &str) -> Self {
462 match code.as_bytes().first().copied() {
463 Some(b'C') => Self::Pseudorange,
464 Some(b'L') => Self::CarrierPhase,
465 Some(b'D') => Self::Doppler,
466 Some(b'S') => Self::SignalStrength,
467 _ => Self::Unknown,
468 }
469 }
470
471 pub fn as_str(self) -> &'static str {
473 match self {
474 Self::Pseudorange => "pseudorange",
475 Self::CarrierPhase => "carrier_phase",
476 Self::Doppler => "doppler",
477 Self::SignalStrength => "signal_strength",
478 Self::Unknown => "unknown",
479 }
480 }
481
482 pub fn units_str(self) -> &'static str {
484 match self {
485 Self::Pseudorange => "meters",
486 Self::CarrierPhase => "cycles",
487 Self::Doppler => "hz",
488 Self::SignalStrength => "db_hz",
489 Self::Unknown => "unknown",
490 }
491 }
492}
493
494#[derive(Debug, Clone, PartialEq)]
496pub struct ObservationValueRow {
497 pub code: String,
499 pub kind: ObservationKind,
501 pub value: Option<f64>,
503 pub lli: Option<u8>,
505 pub ssi: Option<u8>,
507}
508
509#[derive(Debug, Clone, PartialEq)]
511pub struct CarrierPhaseRow {
512 pub code: String,
514 pub value_cycles: Option<f64>,
516 pub lli: Option<u8>,
518 pub ssi: Option<u8>,
520 pub frequency_hz: Option<f64>,
522 pub wavelength_m: Option<f64>,
524 pub value_m: Option<f64>,
526 pub phase_shift_cycles: f64,
530}
531
532pub fn observation_values(
534 obs: &RinexObs,
535 epoch: &ObsEpoch,
536 filter: &ObservationFilter,
537) -> Result<Vec<(GnssSatelliteId, Vec<ObservationValueRow>)>> {
538 let mut out = Vec::new();
539 for (sat, values) in epoch
540 .sats
541 .iter()
542 .filter(|(sat, _)| filter.allowed_codes(sat.system).is_some())
543 {
544 let allowed_codes = filter
545 .allowed_codes(sat.system)
546 .expect("filter presence checked");
547 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
548 continue;
549 };
550 let mut rows = Vec::new();
551 for (code, value) in code_list.iter().zip(values.iter()) {
552 if !allowed_codes.is_empty() && !allowed_codes.iter().any(|c| c == code) {
553 continue;
554 }
555 if let Some(value) = value.value {
556 validate_finite_input(value, "observation.value")?;
557 }
558 let kind = ObservationKind::from_code(code);
559 rows.push(ObservationValueRow {
560 code: code.clone(),
561 kind,
562 value: value.value,
563 lli: value.lli,
564 ssi: value.ssi,
565 });
566 }
567 out.push((*sat, rows));
568 }
569 Ok(out)
570}
571
572pub fn carrier_phase_rows(
574 obs: &RinexObs,
575 epoch: &ObsEpoch,
576 filter: &ObservationFilter,
577) -> Result<Vec<(GnssSatelliteId, Vec<CarrierPhaseRow>)>> {
578 validate_finite_input(obs.header.version, "version")?;
579 let mut out = Vec::new();
580 for (sat, rows) in observation_values(obs, epoch, filter)? {
581 let phases = rows
582 .into_iter()
583 .filter(|row| row.kind == ObservationKind::CarrierPhase)
584 .map(|row| carrier_phase_row(obs, sat, row))
585 .collect::<Result<Vec<_>>>()?;
586 out.push((sat, phases));
587 }
588 Ok(out)
589}
590
591pub fn band_frequency_hz(
596 system: GnssSystem,
597 band: char,
598 glonass_channel: Option<i8>,
599) -> Option<f64> {
600 rinex_band_frequency_hz(system, band, glonass_channel)
601}
602
603pub fn observation_frequency_hz(
605 system: GnssSystem,
606 code: &str,
607 rinex_version: f64,
608 glonass_channel: Option<i8>,
609) -> Result<Option<f64>> {
610 validate_finite_input(rinex_version, "version")?;
611 Ok(rinex_observation_frequency_hz(
612 system,
613 code,
614 rinex_version,
615 glonass_channel,
616 ))
617}
618
619fn carrier_phase_row(
620 obs: &RinexObs,
621 sat: GnssSatelliteId,
622 row: ObservationValueRow,
623) -> Result<CarrierPhaseRow> {
624 let glonass_channel = obs.header.glonass_slots.get(&sat.prn).copied();
625 let frequency_hz =
626 observation_frequency_hz(sat.system, &row.code, obs.header.version, glonass_channel)?;
627 let phase_shift_cycles = phase_shift_cycles(obs, sat, &row.code);
628 let value_cycles = row.value;
629 let wavelength_m =
630 rinex_observation_wavelength_m(sat.system, &row.code, obs.header.version, glonass_channel);
631 let value_m = match value_cycles.zip(wavelength_m) {
632 Some((cycles, lambda)) => {
633 let value_m = cycles * lambda;
634 validate_finite_input(value_m, "carrier_phase.value_m")?;
635 Some(value_m)
636 }
637 None => None,
638 };
639 Ok(CarrierPhaseRow {
640 code: row.code,
641 value_cycles,
642 lli: row.lli,
643 ssi: row.ssi,
644 frequency_hz,
645 wavelength_m,
646 value_m,
647 phase_shift_cycles,
648 })
649}
650
651fn phase_shift_cycles(obs: &RinexObs, sat: GnssSatelliteId, code: &str) -> f64 {
652 let mut system_wide = None;
653 for shift in obs.header.phase_shifts.iter().rev() {
654 if shift.system != sat.system || shift.code != code {
655 continue;
656 }
657 if shift.satellites.is_empty() {
658 if system_wide.is_none() {
659 system_wide = Some(shift.correction_cycles);
660 }
661 } else if shift.satellites.contains(&sat) {
662 return shift.correction_cycles;
663 }
664 }
665 system_wide.unwrap_or(0.0)
666}
667
668pub fn pseudoranges(
675 obs: &RinexObs,
676 epoch: &ObsEpoch,
677 policy: &SignalPolicy,
678) -> Result<Vec<(GnssSatelliteId, f64)>> {
679 let mut out = Vec::new();
680 for (sat, values) in &epoch.sats {
681 let Some(prefs) = policy.codes.get(&sat.system) else {
682 continue;
683 };
684 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
685 continue;
686 };
687 for code in prefs {
688 if let Some(idx) = code_list.iter().position(|c| c == code) {
689 if let Some(ObsValue {
690 value: Some(range_m),
691 ..
692 }) = values.get(idx)
693 {
694 validate_finite_input(*range_m, "pseudorange_m")?;
695 out.push((*sat, *range_m));
696 break;
697 }
698 }
699 }
700 }
701 Ok(out)
702}
703
704struct Parser {
706 version: Option<f64>,
707 is_observation: bool,
708 approx_position_m: Option<[f64; 3]>,
709 antenna_delta_hen_m: Option<[f64; 3]>,
710 obs_codes: BTreeMap<GnssSystem, Vec<String>>,
711 interval_s: Option<f64>,
712 time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
713 time_of_last_obs: Option<(ObsEpochTime, TimeScale)>,
714 program_run_by_date: Option<PgmRunByDate>,
715 comments: Vec<String>,
716 marker_number: Option<String>,
717 marker_type: Option<String>,
718 observer: Option<String>,
719 agency: Option<String>,
720 receiver: Option<ReceiverInfo>,
721 antenna: Option<AntennaInfo>,
722 n_satellites: Option<usize>,
723 prn_obs_counts: BTreeMap<GnssSatelliteId, Vec<Option<usize>>>,
724 prn_obs_counts_current: Option<GnssSatelliteId>,
725 phase_shifts: Vec<ObsPhaseShift>,
726 scale_factors: Vec<ObsScaleFactor>,
727 scale_factor_continuation: Option<ScaleFactorContinuation>,
728 glonass_slots: BTreeMap<u8, i8>,
729 glonass_slots_remaining: Option<usize>,
730 glonass_cod_phs_bis: Option<Vec<(String, f64)>>,
731 signal_strength_unit: Option<String>,
732 leap_seconds: Option<ObsLeapSeconds>,
733 marker_name: Option<String>,
734 unretained_header_labels: Vec<String>,
735 epochs: Vec<ObsEpoch>,
736 current_obs_sys: Option<GnssSystem>,
739 obs_codes_remaining: usize,
741 rinex2_default_system: Option<GnssSystem>,
743 rinex2_obs_codes: Vec<String>,
746 rinex2_obs_codes_remaining: usize,
748 diagnostics: Diagnostics,
753}
754
755#[derive(Debug, Clone, Copy)]
756struct ScaleFactorContinuation {
757 remaining: usize,
758}
759
760impl Parser {
761 fn new() -> Self {
762 Self {
763 version: None,
764 is_observation: false,
765 approx_position_m: None,
766 antenna_delta_hen_m: None,
767 obs_codes: BTreeMap::new(),
768 interval_s: None,
769 time_of_first_obs: None,
770 time_of_last_obs: None,
771 program_run_by_date: None,
772 comments: Vec::new(),
773 marker_number: None,
774 marker_type: None,
775 observer: None,
776 agency: None,
777 receiver: None,
778 antenna: None,
779 n_satellites: None,
780 prn_obs_counts: BTreeMap::new(),
781 prn_obs_counts_current: None,
782 phase_shifts: Vec::new(),
783 scale_factors: Vec::new(),
784 scale_factor_continuation: None,
785 glonass_slots: BTreeMap::new(),
786 glonass_slots_remaining: None,
787 glonass_cod_phs_bis: None,
788 signal_strength_unit: None,
789 leap_seconds: None,
790 marker_name: None,
791 unretained_header_labels: Vec::new(),
792 epochs: Vec::new(),
793 current_obs_sys: None,
794 obs_codes_remaining: 0,
795 rinex2_default_system: None,
796 rinex2_obs_codes: Vec::new(),
797 rinex2_obs_codes_remaining: 0,
798 diagnostics: Diagnostics::new(),
799 }
800 }
801
802 fn is_rinex2(&self) -> bool {
803 self.version
804 .is_some_and(|version| version.floor() as i64 == 2)
805 }
806
807 fn push_unrepresentable_satellite_skip(&mut self, token: &str) {
810 self.diagnostics.push_skip(Skip {
811 at: RecordRef::default().with_satellite(token.trim()),
812 reason: SkipReason::UnrepresentableSatellite,
813 });
814 }
815
816 fn parse_header<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut I) -> Result<()> {
817 let mut saw_end = false;
818 for raw in lines.by_ref() {
819 let raw_line = raw.trim_end_matches(['\r', '\n']);
820 let ascii_line = printable_ascii_header_columns(raw_line);
826 let line = normalize_header_line(&ascii_line);
827 let line = line.as_ref();
828 let label = raw_field_from(line, 60).trim();
829 match label {
830 "RINEX VERSION / TYPE" => self.parse_version(line)?,
831 "PGM / RUN BY / DATE" => self.parse_pgm_run_by_date(line),
832 "COMMENT" => self.comments.push(field(line, 0, 60).trim().to_string()),
833 "APPROX POSITION XYZ" => self.parse_approx_position(line)?,
834 "ANTENNA: DELTA H/E/N" => self.parse_antenna_delta(line)?,
835 "SYS / # / OBS TYPES" => self.parse_obs_types(line)?,
836 "# / TYPES OF OBSERV" => self.parse_obs_types_v2(line)?,
837 "SYS / SCALE FACTOR" => self.parse_scale_factor(line)?,
838 "SYS / PHASE SHIFT" => self.parse_phase_shift(line)?,
839 "TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
840 "TIME OF LAST OBS" => self.parse_time_of_last_obs(line)?,
841 "INTERVAL" => {
842 self.interval_s = optional_f64_field(line, 0, 10, "interval_s")?;
843 }
844 "GLONASS SLOT / FRQ #" => self.parse_glonass_slots(line)?,
845 "GLONASS COD/PHS/BIS" => self.parse_glonass_cod_phs_bis(line)?,
846 "SIGNAL STRENGTH UNIT" => {
847 let unit = field(line, 0, 20).trim();
848 if !unit.is_empty() {
849 self.signal_strength_unit = Some(unit.to_string());
850 }
851 }
852 "LEAP SECONDS" => self.parse_leap_seconds(line)?,
853 "# OF SATELLITES" => {
854 self.n_satellites =
855 Some(strict_int_field::<usize>(line, 0, 6, "n_satellites")?);
856 }
857 "PRN / # OF OBS" => self.parse_prn_obs_counts(line)?,
858 "MARKER NAME" => {
859 let name = field(line, 0, 60).trim();
860 if !name.is_empty() {
861 self.marker_name = Some(name.to_string());
862 }
863 }
864 "MARKER NUMBER" => {
865 self.marker_number = optional_trimmed(line, 0, 20);
866 }
867 "MARKER TYPE" => {
868 self.marker_type = optional_trimmed(line, 0, 20);
869 }
870 "OBSERVER / AGENCY" => {
871 self.observer = optional_trimmed(line, 0, 20);
872 self.agency = optional_trimmed(line, 20, 60);
873 }
874 "REC # / TYPE / VERS" => {
875 self.receiver = Some(ReceiverInfo {
876 number: field(line, 0, 20).trim().to_string(),
877 receiver_type: field(line, 20, 40).trim().to_string(),
878 version: field(line, 40, 60).trim().to_string(),
879 });
880 }
881 "ANT # / TYPE" => {
882 self.antenna = Some(AntennaInfo {
883 number: field(line, 0, 20).trim().to_string(),
884 antenna_type: field(line, 20, 40).trim().to_string(),
885 });
886 }
887 "END OF HEADER" => {
888 self.ensure_obs_type_count_complete(line)?;
889 self.ensure_obs_type_count_complete_v2(line)?;
890 self.ensure_scale_factor_count_complete(line)?;
891 saw_end = true;
892 break;
893 }
894 _ => {
897 if !label.is_empty() {
898 self.unretained_header_labels.push(label.to_string());
899 }
900 }
901 }
902 }
903 if !saw_end {
904 return Err(Error::Parse("RINEX OBS header has no END OF HEADER".into()));
905 }
906 Ok(())
907 }
908
909 fn parse_version(&mut self, line: &str) -> Result<()> {
910 let version_field = field(line, 0, 20).trim();
911 let version = strict_f64_token(version_field, "version", line).or_else(|_| {
912 let token = field(line, 0, 60)
913 .split_whitespace()
914 .next()
915 .ok_or_else(|| Error::Parse(format!("RINEX OBS bad version field in {line:?}")))?;
916 strict_f64_token(token, "version", line)
917 })?;
918 let type_field = field(line, 20, 40);
920 let body = field(line, 0, 60);
921 self.is_observation = type_field.trim_start().starts_with('O')
922 || type_field.contains("OBSERVATION")
923 || body.contains("OBSERVATION")
924 || body.split_whitespace().any(|token| token == "O");
925 if !self.is_observation {
926 return Err(Error::Parse(format!(
927 "RINEX file is not observation data: {type_field:?}"
928 )));
929 }
930 if !matches!(version.floor() as i64, 2..=4) {
931 return Err(Error::Parse(format!(
932 "RINEX OBS parser requires major version 2, 3, or 4, got {version}"
933 )));
934 }
935 if version.floor() as i64 == 2 {
936 let system_field = field(line, 40, 41).trim();
937 if let Some(letter) = system_field.chars().next().filter(|letter| *letter != 'M') {
938 self.rinex2_default_system = GnssSystem::from_letter(letter);
939 }
940 }
941 self.version = Some(version);
942 Ok(())
943 }
944
945 fn parse_approx_position(&mut self, line: &str) -> Result<()> {
946 let body = field(line, 0, 60);
947 self.approx_position_m = Some(strict_vec3_tokens(
948 body,
949 line,
950 [
951 "approx_position.x_m",
952 "approx_position.y_m",
953 "approx_position.z_m",
954 ],
955 )?);
956 Ok(())
957 }
958
959 fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
960 let body = field(line, 0, 60);
961 self.antenna_delta_hen_m = Some(strict_vec3_tokens(
962 body,
963 line,
964 [
965 "antenna_delta.height_m",
966 "antenna_delta.east_m",
967 "antenna_delta.north_m",
968 ],
969 )?);
970 Ok(())
971 }
972
973 fn parse_pgm_run_by_date(&mut self, line: &str) {
974 self.program_run_by_date = Some(PgmRunByDate {
975 program: field(line, 0, 20).trim().to_string(),
976 run_by: field(line, 20, 40).trim().to_string(),
977 date: field(line, 40, 60).trim().to_string(),
978 });
979 }
980
981 fn parse_obs_types(&mut self, line: &str) -> Result<()> {
982 let sys_field = field(line, 0, 1).trim();
986 if !sys_field.is_empty() {
987 let count = match strict_int_field::<usize>(line, 3, 6, "obs_type_count") {
988 Ok(count) => count,
989 Err(_) => return self.parse_obs_types_whitespace(line),
990 };
991 self.ensure_obs_type_count_complete(line)?;
992 let letter = sys_field.chars().next().unwrap();
993 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
994 Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
995 })?;
996 self.ensure_obs_type_count_fits(system, count, line)?;
997 self.current_obs_sys = Some(system);
998 self.obs_codes_remaining = count;
999 self.obs_codes.entry(system).or_default();
1000 }
1001 let Some(system) = self.current_obs_sys else {
1002 return Ok(());
1003 };
1004 let codes_section = field(line, 7, 60);
1007 let list = self.obs_codes.get_mut(&system).expect("system inserted");
1008 for tok in codes_section.split_whitespace() {
1009 if self.obs_codes_remaining == 0 {
1010 return Err(Error::Parse(format!(
1011 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
1012 )));
1013 }
1014 list.push(obs_code_token(tok, "SYS / # / OBS TYPES", line)?);
1015 self.obs_codes_remaining -= 1;
1016 }
1017 Ok(())
1018 }
1019
1020 fn parse_obs_types_v2(&mut self, line: &str) -> Result<()> {
1021 if field(line, 0, 6).trim().is_empty() {
1022 if self.rinex2_obs_codes_remaining == 0 {
1023 return Ok(());
1024 }
1025 } else {
1026 self.ensure_obs_type_count_complete_v2(line)?;
1027 let count = strict_int_field::<usize>(line, 0, 6, "rinex2.obs_type_count")?;
1028 if count > MAX_OBS_TYPE_COUNT {
1029 return Err(Error::Parse(format!(
1030 "RINEX OBS # / TYPES OF OBSERV declares {count} codes, exceeding the {MAX_OBS_TYPE_COUNT} the SYS / # / OBS TYPES I3 field can carry, in {line:?}"
1031 )));
1032 }
1033 self.rinex2_obs_codes.clear();
1034 self.rinex2_obs_codes_remaining = count;
1035 }
1036 for code in field(line, 6, 60).split_whitespace() {
1037 if self.rinex2_obs_codes_remaining == 0 {
1038 return Err(Error::Parse(format!(
1039 "RINEX OBS # / TYPES OF OBSERV lists more codes than declared in {line:?}"
1040 )));
1041 }
1042 self.rinex2_obs_codes
1043 .push(obs_code_token(code, "# / TYPES OF OBSERV", line)?);
1044 self.rinex2_obs_codes_remaining -= 1;
1045 }
1046 Ok(())
1047 }
1048
1049 fn parse_obs_types_whitespace(&mut self, line: &str) -> Result<()> {
1050 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
1051 if tokens.is_empty() {
1052 return Err(Error::Parse(format!(
1053 "RINEX OBS malformed SYS / # / OBS TYPES record: {line:?}"
1054 )));
1055 }
1056
1057 if tokens.len() >= 2 && tokens[0].len() == 1 {
1058 if let Ok(count) = strict_int_token::<usize>(tokens[1], "obs_type_count", line) {
1059 let letter = tokens[0]
1060 .chars()
1061 .next()
1062 .ok_or_else(|| Error::Parse("RINEX OBS missing system letter".into()))?;
1063 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
1064 Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
1065 })?;
1066 self.ensure_obs_type_count_complete(line)?;
1067 self.ensure_obs_type_count_fits(system, count, line)?;
1068 self.current_obs_sys = Some(system);
1069 self.obs_codes_remaining = count;
1070 self.obs_codes.entry(system).or_default();
1071 return self.push_obs_type_tokens(system, &tokens[2..], line);
1072 }
1073 }
1074
1075 let Some(system) = self.current_obs_sys else {
1076 return Err(Error::Parse(format!(
1077 "RINEX OBS malformed SYS / # / OBS TYPES record: {line:?}"
1078 )));
1079 };
1080 if self.obs_codes_remaining == 0 {
1081 return Err(Error::Parse(format!(
1082 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
1083 )));
1084 }
1085 self.push_obs_type_tokens(system, &tokens, line)
1086 }
1087
1088 fn push_obs_type_tokens(
1089 &mut self,
1090 system: GnssSystem,
1091 codes: &[&str],
1092 line: &str,
1093 ) -> Result<()> {
1094 let list = self.obs_codes.entry(system).or_default();
1095 for code in codes {
1096 if self.obs_codes_remaining == 0 {
1097 return Err(Error::Parse(format!(
1098 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
1099 )));
1100 }
1101 list.push(obs_code_token(code, "SYS / # / OBS TYPES", line)?);
1102 self.obs_codes_remaining -= 1;
1103 }
1104 Ok(())
1105 }
1106
1107 fn ensure_obs_type_count_fits(
1110 &self,
1111 system: GnssSystem,
1112 count: usize,
1113 line: &str,
1114 ) -> Result<()> {
1115 let collected = self.obs_codes.get(&system).map_or(0, Vec::len);
1116 let total = collected.saturating_add(count);
1117 if total > MAX_OBS_TYPE_COUNT {
1118 return Err(Error::Parse(format!(
1119 "RINEX OBS {system} SYS / # / OBS TYPES declares {total} codes, exceeding the I3 field maximum of {MAX_OBS_TYPE_COUNT} in {line:?}"
1120 )));
1121 }
1122 Ok(())
1123 }
1124
1125 fn ensure_obs_type_count_complete(&self, line: &str) -> Result<()> {
1126 if self.obs_codes_remaining == 0 {
1127 return Ok(());
1128 }
1129 let Some(system) = self.current_obs_sys else {
1130 return Ok(());
1131 };
1132 let supplied = self.obs_codes.get(&system).map_or(0, Vec::len);
1133 let declared = supplied + self.obs_codes_remaining;
1134 Err(Error::Parse(format!(
1135 "RINEX OBS {system} SYS / # / OBS TYPES declares {declared} codes but supplies {supplied} before {line:?}"
1136 )))
1137 }
1138
1139 fn ensure_obs_type_count_complete_v2(&self, line: &str) -> Result<()> {
1140 if self.rinex2_obs_codes_remaining == 0 {
1141 return Ok(());
1142 }
1143 let supplied = self.rinex2_obs_codes.len();
1144 let declared = supplied + self.rinex2_obs_codes_remaining;
1145 Err(Error::Parse(format!(
1146 "RINEX OBS # / TYPES OF OBSERV declares {declared} codes but supplies {supplied} before {line:?}"
1147 )))
1148 }
1149
1150 fn parse_phase_shift(&mut self, line: &str) -> Result<()> {
1151 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
1152 if tokens.is_empty() {
1153 return Ok(());
1154 }
1155 if tokens.len() < 2 {
1156 return Err(Error::Parse(format!(
1157 "RINEX OBS phase-shift header has too few fields in {line:?}"
1158 )));
1159 }
1160
1161 let system = tokens[0]
1162 .chars()
1163 .next()
1164 .and_then(GnssSystem::from_letter)
1165 .ok_or_else(|| {
1166 Error::Parse(format!(
1167 "RINEX OBS phase-shift system unparsable in {line:?}"
1168 ))
1169 })?;
1170 let code = obs_code_token(tokens[1], "SYS / PHASE SHIFT", line)?;
1171 let correction_cycles = match tokens.get(2) {
1172 Some(token) => strict_f64_token(token, "phase_shift.correction_cycles", line)?,
1173 None => 0.0,
1174 };
1175
1176 let satellites = if let Some(count_token) = tokens.get(3) {
1177 let count =
1178 strict_int_token::<usize>(count_token, "phase_shift.satellite_count", line)?;
1179 let sat_tokens = &tokens[4..];
1180 if sat_tokens.len() != count {
1181 return Err(Error::Parse(format!(
1182 "RINEX OBS phase-shift satellite count mismatch in {line:?}"
1183 )));
1184 }
1185 sat_tokens
1186 .iter()
1187 .map(|token| {
1188 parse_sv_token(token).ok_or_else(|| {
1189 Error::Parse(format!(
1190 "RINEX OBS phase-shift satellite token {token:?} unparsable in {line:?}"
1191 ))
1192 })
1193 })
1194 .collect::<Result<Vec<_>>>()?
1195 } else {
1196 Vec::new()
1197 };
1198
1199 let shift = ObsPhaseShift {
1200 system,
1201 code,
1202 correction_cycles,
1203 satellites,
1204 };
1205 let content_width = write::phase_shift_content(&shift).len();
1210 if content_width > write::HEADER_CONTENT_WIDTH {
1211 return Err(Error::Parse(format!(
1212 "RINEX OBS SYS / PHASE SHIFT record needs {content_width} columns, exceeding the {} a header record carries, in {line:?}",
1213 write::HEADER_CONTENT_WIDTH
1214 )));
1215 }
1216 self.phase_shifts.push(shift);
1217 Ok(())
1218 }
1219
1220 fn parse_scale_factor(&mut self, line: &str) -> Result<()> {
1221 let sys_field = field(line, 0, 1).trim();
1222 if !sys_field.is_empty() {
1223 self.ensure_scale_factor_count_complete(line)?;
1224 let letter = sys_field.chars().next().unwrap();
1225 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
1226 Error::Parse(format!("RINEX OBS unknown scale-factor system {letter:?}"))
1227 })?;
1228 let factor =
1229 scale_factor_value(strict_int_field::<u32>(line, 2, 6, "scale_factor.factor")?)?;
1230 let count_field = field(line, 8, 10).trim();
1231 let count = if count_field.is_empty() {
1232 0
1233 } else {
1234 strict_int_token::<usize>(count_field, "scale_factor.obs_type_count", line)?
1235 };
1236 self.scale_factors.push(ObsScaleFactor {
1237 system,
1238 factor,
1239 codes: Vec::new(),
1240 });
1241 if count == 0 {
1242 return Ok(());
1243 }
1244 self.scale_factor_continuation = Some(ScaleFactorContinuation { remaining: count });
1245 }
1246
1247 self.collect_scale_factor_codes(line)
1248 }
1249
1250 fn collect_scale_factor_codes(&mut self, line: &str) -> Result<()> {
1251 let Some(mut continuation) = self.scale_factor_continuation else {
1252 return Ok(());
1253 };
1254 let record = self
1255 .scale_factors
1256 .last_mut()
1257 .expect("scale factor continuation has a record");
1258 for code in field(line, 10, 60).split_whitespace() {
1259 if continuation.remaining == 0 {
1260 return Err(Error::Parse(format!(
1261 "RINEX OBS SYS / SCALE FACTOR lists more codes than declared in {line:?}"
1262 )));
1263 }
1264 record
1265 .codes
1266 .push(obs_code_token(code, "SYS / SCALE FACTOR", line)?);
1267 continuation.remaining -= 1;
1268 }
1269 self.scale_factor_continuation = (continuation.remaining > 0).then_some(continuation);
1270 Ok(())
1271 }
1272
1273 fn ensure_scale_factor_count_complete(&self, line: &str) -> Result<()> {
1274 let Some(continuation) = self.scale_factor_continuation else {
1275 return Ok(());
1276 };
1277 let supplied = self
1278 .scale_factors
1279 .last()
1280 .map_or(0, |record| record.codes.len());
1281 let declared = supplied + continuation.remaining;
1282 Err(Error::Parse(format!(
1283 "RINEX OBS SYS / SCALE FACTOR declares {declared} codes but supplies {supplied} before {line:?}"
1284 )))
1285 }
1286
1287 fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
1288 self.time_of_first_obs = Some(self.parse_time_header(line, "time_of_first_obs")?);
1289 Ok(())
1290 }
1291
1292 fn parse_time_of_last_obs(&mut self, line: &str) -> Result<()> {
1293 self.time_of_last_obs = Some(self.parse_time_header(line, "time_of_last_obs")?);
1294 Ok(())
1295 }
1296
1297 fn parse_time_header(
1298 &self,
1299 line: &str,
1300 prefix: &'static str,
1301 ) -> Result<(ObsEpochTime, TimeScale)> {
1302 let body = field(line, 0, 43);
1303 let scale_label = field(line, 48, 51).trim();
1304 let scale = time_scale_from_label(scale_label, line)?;
1305 let year = match prefix {
1306 "time_of_last_obs" => "time_of_last_obs.year",
1307 _ => "time_of_first_obs.year",
1308 };
1309 let month = match prefix {
1310 "time_of_last_obs" => "time_of_last_obs.month",
1311 _ => "time_of_first_obs.month",
1312 };
1313 let day = match prefix {
1314 "time_of_last_obs" => "time_of_last_obs.day",
1315 _ => "time_of_first_obs.day",
1316 };
1317 let hour = match prefix {
1318 "time_of_last_obs" => "time_of_last_obs.hour",
1319 _ => "time_of_first_obs.hour",
1320 };
1321 let minute = match prefix {
1322 "time_of_last_obs" => "time_of_last_obs.minute",
1323 _ => "time_of_first_obs.minute",
1324 };
1325 let second = match prefix {
1326 "time_of_last_obs" => "time_of_last_obs.second",
1327 _ => "time_of_first_obs.second",
1328 };
1329 let epoch = parse_epoch_time_tokens(
1330 body,
1331 line,
1332 [year, month, day, hour, minute, second],
1333 civil_second_policy_for_time_scale(scale),
1334 )?;
1335 Ok((epoch, scale))
1336 }
1337
1338 fn parse_glonass_slots(&mut self, line: &str) -> Result<()> {
1339 let count_field = field(line, 0, 3).trim();
1341 if !count_field.is_empty() {
1342 let count = strict_int_token::<usize>(count_field, "glonass_slot.count", line)?;
1343 self.glonass_slots_remaining = Some(count);
1344 }
1345 let body = field(line, 4, 60);
1346 let tokens: Vec<&str> = body.split_whitespace().collect();
1347 if !tokens.len().is_multiple_of(2) {
1348 return Err(Error::Parse(format!(
1349 "RINEX OBS GLONASS slot table has an odd token count in {line:?}"
1350 )));
1351 }
1352 for pair in tokens.chunks_exact(2) {
1353 if let Some(remaining) = self.glonass_slots_remaining.as_mut() {
1357 if *remaining == 0 {
1358 return Err(Error::Parse(format!(
1359 "RINEX OBS GLONASS slot table has more entries than declared in {line:?}"
1360 )));
1361 }
1362 *remaining -= 1;
1363 }
1364 let Some(sat) = parse_sv_token(pair[0]) else {
1370 self.push_unrepresentable_satellite_skip(pair[0]);
1371 continue;
1372 };
1373 if sat.system != GnssSystem::Glonass {
1374 return Err(Error::Parse(format!(
1375 "RINEX OBS GLONASS slot token {:?} is not GLONASS in {line:?}",
1376 pair[0]
1377 )));
1378 }
1379 let channel = strict_int_token::<i8>(pair[1], "glonass_slot.channel", line)?;
1380 if !valid_glonass_frequency_channel(i32::from(channel)) {
1381 return Err(Error::Parse(format!(
1382 "RINEX OBS invalid glonass_slot.channel: {channel} out of range in {line:?}"
1383 )));
1384 }
1385 self.glonass_slots.insert(sat.prn, channel);
1386 }
1387 Ok(())
1388 }
1389
1390 fn parse_glonass_cod_phs_bis(&mut self, line: &str) -> Result<()> {
1391 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
1392 let mut entries = Vec::new();
1393 for pair in tokens.chunks(2) {
1394 if pair.len() != 2 {
1395 return Err(Error::Parse(format!(
1396 "RINEX OBS GLONASS COD/PHS/BIS has an odd token count in {line:?}"
1397 )));
1398 }
1399 entries.push((
1400 pair[0].to_string(),
1401 strict_f64_token(pair[1], "glonass_code_phase_bias", line)?,
1402 ));
1403 }
1404 self.glonass_cod_phs_bis = Some(entries);
1405 Ok(())
1406 }
1407
1408 fn parse_leap_seconds(&mut self, line: &str) -> Result<()> {
1409 let current = strict_int_field::<i64>(line, 0, 6, "leap_seconds.current")?;
1410 self.leap_seconds = Some(ObsLeapSeconds {
1411 current,
1412 delta_future: optional_i64_field(line, 6, 12, "leap_seconds.delta_future")?,
1413 week: optional_i64_field(line, 12, 18, "leap_seconds.week")?,
1414 day: optional_i64_field(line, 18, 24, "leap_seconds.day")?,
1415 });
1416 Ok(())
1417 }
1418
1419 fn parse_prn_obs_counts(&mut self, line: &str) -> Result<()> {
1420 let token = field(line, 0, 3).trim();
1421 let sat = if token.is_empty() {
1422 let Some(sat) = self.prn_obs_counts_current else {
1423 return Ok(());
1424 };
1425 sat
1426 } else {
1427 let Some(sat) = parse_sv_token(token) else {
1428 self.prn_obs_counts_current = None;
1429 self.push_unrepresentable_satellite_skip(token);
1430 return Ok(());
1431 };
1432 self.prn_obs_counts_current = Some(sat);
1433 sat
1434 };
1435 let count = self.obs_codes.get(&sat.system).map_or(0, Vec::len);
1436 let already = self.prn_obs_counts.get(&sat).map_or(0, Vec::len);
1437 let remaining = count.saturating_sub(already);
1438 let mut values = Vec::with_capacity(remaining.min(9));
1439 for idx in 0..remaining {
1440 let start = 3 + idx * 6;
1441 if start + 6 > 60 {
1442 break;
1443 }
1444 let raw = field(line, start, start + 6).trim();
1445 if raw.is_empty() {
1446 values.push(None);
1447 } else {
1448 values.push(Some(strict_int_token::<usize>(raw, "prn_obs_count", line)?));
1449 }
1450 }
1451 self.prn_obs_counts.entry(sat).or_default().extend(values);
1452 Ok(())
1453 }
1454
1455 fn parse_body<'a, I: Iterator<Item = &'a str>>(
1456 &mut self,
1457 lines: &mut std::iter::Peekable<I>,
1458 ) -> Result<()> {
1459 while let Some(raw) = lines.next() {
1460 let line = raw.trim_end_matches(['\r', '\n']);
1461 if line.is_empty() {
1462 continue;
1463 }
1464 if !line.starts_with('>') {
1465 continue;
1467 }
1468 let time_scale = self
1469 .time_of_first_obs
1470 .map_or(TimeScale::Gpst, |(_, scale)| scale);
1471 let (epoch_time, flag, numsat, rcv_clock_offset_s, epoch_picoseconds) =
1472 parse_epoch_line(line, civil_second_policy_for_time_scale(time_scale))?;
1473
1474 if flag > 1 {
1475 for _ in 0..numsat {
1479 lines
1480 .next()
1481 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
1482 }
1483 self.epochs.push(ObsEpoch {
1484 epoch: epoch_time,
1485 flag,
1486 rcv_clock_offset_s,
1487 epoch_picoseconds,
1488 declared_record_count: numsat,
1489 special_record_count: numsat,
1490 sats: BTreeMap::new(),
1491 });
1492 continue;
1493 }
1494
1495 let mut sats = BTreeMap::new();
1496 for _ in 0..numsat {
1497 let sat_line = lines.next().ok_or_else(|| {
1498 Error::Parse("RINEX OBS epoch truncated: missing satellite line".into())
1499 })?;
1500 let sat_line = sat_line.trim_end_matches(['\r', '\n']);
1501 let normalized = ascii_fixed_columns(sat_line);
1508 if !starts_with_sat_designator(&normalized) {
1509 return Err(Error::Parse(
1514 "RINEX OBS epoch truncated: expected satellite record".into(),
1515 ));
1516 }
1517 if parse_sv_token(field(&normalized, 0, 3)).is_none() {
1518 self.push_unrepresentable_satellite_skip(field(&normalized, 0, 3));
1523 consume_skipped_sat_continuations(lines);
1524 continue;
1525 }
1526 let sat_record = self.collect_sat_record(sat_line, lines)?;
1527 let (sat, values) = self.parse_sat_line(&sat_record)?;
1528 sats.insert(sat, values);
1529 }
1530 self.epochs.push(ObsEpoch {
1531 epoch: epoch_time,
1532 flag,
1533 rcv_clock_offset_s,
1534 epoch_picoseconds,
1535 declared_record_count: numsat,
1536 special_record_count: 0,
1537 sats,
1538 });
1539 }
1540 Ok(())
1541 }
1542
1543 fn parse_body_v2<'a, I: Iterator<Item = &'a str>>(
1544 &mut self,
1545 lines: &mut std::iter::Peekable<I>,
1546 ) -> Result<()> {
1547 while let Some(raw) = lines.next() {
1548 let line = raw.trim_end_matches(['\r', '\n']);
1549 if line.is_empty() {
1550 continue;
1551 }
1552 let time_scale = self
1553 .time_of_first_obs
1554 .map_or(TimeScale::Gpst, |(_, scale)| scale);
1555 let (epoch_time, flag, numsat, rcv_clock_offset_s) =
1556 parse_epoch_line_v2(line, civil_second_policy_for_time_scale(time_scale))?;
1557
1558 if flag > 1 {
1559 for _ in 0..numsat {
1560 lines
1561 .next()
1562 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
1563 }
1564 self.epochs.push(ObsEpoch {
1565 epoch: epoch_time,
1566 flag,
1567 rcv_clock_offset_s,
1568 epoch_picoseconds: None,
1569 declared_record_count: numsat,
1570 special_record_count: numsat,
1571 sats: BTreeMap::new(),
1572 });
1573 continue;
1574 }
1575
1576 let sv_tokens = collect_epoch_sv_tokens_v2(line, numsat, lines)?;
1577 let obs_lines_per_sat = self.rinex2_obs_lines_per_sat()?;
1578 let mut sats = BTreeMap::new();
1579 for token in sv_tokens {
1580 let mut obs_lines = Vec::with_capacity(obs_lines_per_sat);
1581 for _ in 0..obs_lines_per_sat {
1582 let obs_line = lines.next().ok_or_else(|| {
1583 Error::Parse("RINEX OBS epoch truncated: missing observation line".into())
1584 })?;
1585 obs_lines.push(obs_line.trim_end_matches(['\r', '\n']).to_string());
1586 }
1587
1588 let Some(sat) = self.parse_sv_token_v2(&token) else {
1589 self.push_unrepresentable_satellite_skip(&token);
1590 continue;
1591 };
1592 self.ensure_rinex2_system_obs_codes(sat.system);
1593 let values = self.parse_sat_obs_v2(sat.system, &obs_lines)?;
1594 sats.insert(sat, values);
1595 }
1596 self.epochs.push(ObsEpoch {
1597 epoch: epoch_time,
1598 flag,
1599 rcv_clock_offset_s,
1600 epoch_picoseconds: None,
1601 declared_record_count: numsat,
1602 special_record_count: 0,
1603 sats,
1604 });
1605 }
1606 Ok(())
1607 }
1608
1609 fn rinex2_obs_lines_per_sat(&self) -> Result<usize> {
1610 if self.rinex2_obs_codes.is_empty() {
1611 return Err(Error::Parse(
1612 "RINEX OBS header has no # / TYPES OF OBSERV records".into(),
1613 ));
1614 }
1615 Ok(self.rinex2_obs_codes.len().div_ceil(5))
1616 }
1617
1618 fn parse_sv_token_v2(&self, token: &str) -> Option<GnssSatelliteId> {
1619 parse_sv_token_v2(token, self.rinex2_default_system.unwrap_or(GnssSystem::Gps))
1620 }
1621
1622 fn ensure_rinex2_system_obs_codes(&mut self, system: GnssSystem) {
1623 self.obs_codes.entry(system).or_insert_with(|| {
1624 self.rinex2_obs_codes
1625 .iter()
1626 .map(|code| canonical_rinex2_obs_code(system, code))
1627 .collect()
1628 });
1629 }
1630
1631 fn parse_sat_obs_v2(&self, system: GnssSystem, obs_lines: &[String]) -> Result<Vec<ObsValue>> {
1632 let code_list = self.obs_codes.get(&system).ok_or_else(|| {
1633 Error::Parse(format!(
1634 "RINEX OBS satellite system {system} has no canonical observation-code table"
1635 ))
1636 })?;
1637 let mut values = Vec::with_capacity(code_list.len());
1638 for (i, code) in code_list.iter().enumerate() {
1639 let line = obs_lines.get(i / 5).map_or("", String::as_str);
1640 let start = (i % 5) * OBS_FIELD_WIDTH;
1641 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1642 let value = if value_str.is_empty() {
1643 None
1644 } else {
1645 let scale = self.scale_factor_for(system, code);
1646 let parsed = strict_f64_token(value_str, "observation.value", line)? / scale;
1647 if format!("{:.3}", parsed * scale).len() > OBS_VALUE_WIDTH {
1648 return Err(Error::Parse(
1649 "RINEX OBS observation value exceeds the F14.3 field width".into(),
1650 ));
1651 }
1652 Some(parsed)
1653 };
1654 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1655 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1656 values.push(ObsValue { value, lli, ssi });
1657 }
1658 Ok(values)
1659 }
1660
1661 fn collect_sat_record<'a, I: Iterator<Item = &'a str>>(
1662 &self,
1663 first_line: &str,
1664 lines: &mut std::iter::Peekable<I>,
1665 ) -> Result<String> {
1666 let first_line = ascii_fixed_columns(first_line);
1667 let token = field(&first_line, 0, 3);
1668 let sat = parse_sv_token(token).ok_or_else(|| {
1669 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1670 })?;
1671 let n_obs = self.obs_count_for_sat(sat)?;
1672 let mut record = first_line.into_owned();
1673
1674 while sat_record_field_count(record.len()) < n_obs {
1675 let Some(raw_next) = lines.peek().copied() else {
1676 break;
1677 };
1678 let next = raw_next.trim_end_matches(['\r', '\n']);
1679 let next = ascii_fixed_columns(next);
1680 if next.starts_with('>') || starts_with_sat_designator(&next) {
1687 break;
1688 }
1689 let continuation = lines.next().expect("peeked continuation line");
1690 let continuation = ascii_fixed_columns(continuation.trim_end_matches(['\r', '\n']));
1691 append_sat_continuation(&mut record, &continuation, n_obs);
1692 }
1693
1694 Ok(record)
1695 }
1696
1697 fn obs_count_for_sat(&self, sat: GnssSatelliteId) -> Result<usize> {
1698 self.obs_codes
1699 .get(&sat.system)
1700 .map(Vec::len)
1701 .ok_or_else(|| {
1702 Error::Parse(format!(
1703 "RINEX OBS satellite {sat} uses undeclared observation system"
1704 ))
1705 })
1706 }
1707
1708 fn parse_sat_line(&self, line: &str) -> Result<(GnssSatelliteId, Vec<ObsValue>)> {
1709 let token = field(line, 0, 3);
1710 let sat = parse_sv_token(token).ok_or_else(|| {
1711 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1712 })?;
1713 let code_list = self.obs_codes.get(&sat.system).ok_or_else(|| {
1714 Error::Parse(format!(
1715 "RINEX OBS satellite {sat} uses undeclared observation system"
1716 ))
1717 })?;
1718 let mut values = Vec::with_capacity(code_list.len());
1719 for (i, code) in code_list.iter().enumerate() {
1720 let start = 3 + i * OBS_FIELD_WIDTH;
1721 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1722 let value = if value_str.is_empty() {
1723 None
1724 } else {
1725 let scale = self.scale_factor_for(sat.system, code);
1726 let parsed = strict_f64_token(value_str, "observation.value", line)? / scale;
1727 if format!("{:.3}", parsed * scale).len() > OBS_VALUE_WIDTH {
1734 return Err(Error::Parse(
1735 "RINEX OBS observation value exceeds the F14.3 field width".into(),
1736 ));
1737 }
1738 Some(parsed)
1739 };
1740 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1741 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1742 values.push(ObsValue { value, lli, ssi });
1743 }
1744 Ok((sat, values))
1745 }
1746
1747 fn finish(self) -> Result<RinexObs> {
1748 let version = self
1749 .version
1750 .ok_or_else(|| Error::Parse("RINEX OBS missing RINEX VERSION / TYPE".into()))?;
1751 if let Some(remaining) = self.glonass_slots_remaining {
1752 if remaining != 0 {
1753 return Err(Error::Parse(format!(
1754 "RINEX OBS GLONASS slot table missing {remaining} declared entries"
1755 )));
1756 }
1757 }
1758 let mut obs_codes = self.obs_codes;
1759 if obs_codes.is_empty() && !self.rinex2_obs_codes.is_empty() {
1760 let system = self.rinex2_default_system.unwrap_or(GnssSystem::Gps);
1761 obs_codes.insert(
1762 system,
1763 self.rinex2_obs_codes
1764 .iter()
1765 .map(|code| canonical_rinex2_obs_code(system, code))
1766 .collect(),
1767 );
1768 }
1769 if obs_codes.is_empty() {
1770 return Err(Error::Parse(
1771 "RINEX OBS header has no SYS / # / OBS TYPES records".into(),
1772 ));
1773 }
1774 let header = ObsHeader {
1775 version,
1776 approx_position_m: self.approx_position_m,
1777 antenna_delta_hen_m: self.antenna_delta_hen_m,
1778 obs_codes,
1779 program_run_by_date: self.program_run_by_date,
1780 comments: self.comments,
1781 marker_number: self.marker_number,
1782 marker_type: self.marker_type,
1783 observer: self.observer,
1784 agency: self.agency,
1785 receiver: self.receiver,
1786 antenna: self.antenna,
1787 interval_s: self.interval_s,
1788 time_of_first_obs: self.time_of_first_obs,
1789 time_of_last_obs: self.time_of_last_obs,
1790 n_satellites: self.n_satellites,
1791 prn_obs_counts: self.prn_obs_counts,
1792 phase_shifts: self.phase_shifts,
1793 scale_factors: self.scale_factors,
1794 glonass_slots: self.glonass_slots,
1795 glonass_cod_phs_bis: self.glonass_cod_phs_bis,
1796 signal_strength_unit: self.signal_strength_unit,
1797 leap_seconds: self.leap_seconds,
1798 marker_name: self.marker_name,
1799 unretained_header_labels: self.unretained_header_labels,
1800 };
1801 Ok(RinexObs {
1802 header,
1803 epochs: self.epochs,
1804 skipped_records: self.diagnostics.skips.len(),
1805 })
1806 }
1807
1808 fn scale_factor_for(&self, system: GnssSystem, code: &str) -> f64 {
1809 self.scale_factors
1810 .iter()
1811 .rev()
1812 .find(|record| {
1813 record.system == system
1814 && (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
1815 })
1816 .map_or(1.0, |record| record.factor)
1817 }
1818}
1819
1820fn normalize_header_line(line: &str) -> Cow<'_, str> {
1821 let fixed_label = raw_field_from(line, 60).trim();
1822 if HEADER_LABELS.contains(&fixed_label) {
1823 return Cow::Borrowed(line);
1824 }
1825
1826 for &label in HEADER_LABELS {
1827 let Some(index) = line.rfind(label) else {
1828 continue;
1829 };
1830 if !line[index + label.len()..].trim().is_empty() {
1831 continue;
1832 }
1833 let content = line[..index].trim_end();
1834 let content = truncate_header_content(content);
1835 return Cow::Owned(format!("{content:<60}{label}"));
1836 }
1837
1838 Cow::Borrowed(line)
1839}
1840
1841fn printable_ascii_header_columns(line: &str) -> Cow<'_, str> {
1844 if line
1845 .bytes()
1846 .all(|byte| byte == b' ' || byte.is_ascii_graphic())
1847 {
1848 return Cow::Borrowed(line);
1849 }
1850
1851 let mut normalized = String::with_capacity(line.len());
1852 for ch in line.chars() {
1853 if ch == ' ' || ch.is_ascii_graphic() {
1854 normalized.push(ch);
1855 } else {
1856 for _ in 0..ch.len_utf8() {
1860 normalized.push(' ');
1861 }
1862 }
1863 }
1864 Cow::Owned(normalized)
1865}
1866
1867fn truncate_header_content(content: &str) -> Cow<'_, str> {
1868 if content.len() <= 60 {
1869 return Cow::Borrowed(content);
1870 }
1871 let mut end = 60;
1872 while !content.is_char_boundary(end) {
1873 end -= 1;
1874 }
1875 Cow::Owned(content[..end].to_string())
1876}
1877
1878type ParsedEpochLine = (ObsEpochTime, u8, usize, Option<f64>, Option<u32>);
1881
1882fn parse_epoch_line(
1883 line: &str,
1884 second_policy: validate::CivilSecondPolicy,
1885) -> Result<ParsedEpochLine> {
1886 let body = line
1887 .strip_prefix('>')
1888 .ok_or_else(|| Error::Parse(format!("RINEX OBS epoch line lacks '>': {line:?}")))?;
1889 let tokens: Vec<&str> = body.split_whitespace().collect();
1890 if tokens.len() < 8 {
1891 return Err(Error::Parse(format!(
1892 "RINEX OBS epoch line has too few fields in {line:?}"
1893 )));
1894 }
1895 let epoch = parse_epoch_time_tokens(
1896 &tokens[..6].join(" "),
1897 line,
1898 [
1899 "epoch.year",
1900 "epoch.month",
1901 "epoch.day",
1902 "epoch.hour",
1903 "epoch.minute",
1904 "epoch.second",
1905 ],
1906 second_policy,
1907 )?;
1908
1909 let mut index = 6;
1910 let epoch_picoseconds = if tokens
1911 .get(index)
1912 .is_some_and(|token| token.len() == 5 && token.bytes().all(|b| b.is_ascii_digit()))
1913 && tokens.len() >= 9
1914 {
1915 let value = strict_int_token::<u32>(tokens[index], "epoch.picoseconds", line)?;
1916 index += 1;
1917 Some(value)
1918 } else {
1919 None
1920 };
1921 let flag = strict_int_token::<u8>(tokens[index], "epoch.flag", line)?;
1922 index += 1;
1923 let numsat = parse_epoch_record_count(tokens[index], line)?;
1924 index += 1;
1925 let rcv_clock_offset_s = tokens
1926 .get(index)
1927 .map(|token| strict_f64_token(token, "epoch.rcv_clock_offset_s", line))
1928 .transpose()?;
1929 Ok((epoch, flag, numsat, rcv_clock_offset_s, epoch_picoseconds))
1930}
1931
1932type ParsedEpochLineV2 = (ObsEpochTime, u8, usize, Option<f64>);
1933
1934fn parse_epoch_line_v2(
1935 line: &str,
1936 second_policy: validate::CivilSecondPolicy,
1937) -> Result<ParsedEpochLineV2> {
1938 let head = field(line, 0, 32);
1939 let tokens: Vec<&str> = head.split_whitespace().collect();
1940 if tokens.len() < 8 {
1941 return Err(Error::Parse(format!(
1942 "RINEX OBS v2 epoch line has too few fields in {line:?}"
1943 )));
1944 }
1945 let year = strict_int_token::<i32>(tokens[0], "epoch.year", line)?;
1946 let year = expand_rinex2_year(year);
1947 let month = strict_int_token::<i64>(tokens[1], "epoch.month", line)?;
1948 let day = strict_int_token::<i64>(tokens[2], "epoch.day", line)?;
1949 let hour = strict_int_token::<i64>(tokens[3], "epoch.hour", line)?;
1950 let minute = strict_int_token::<i64>(tokens[4], "epoch.minute", line)?;
1951 let second = strict_f64_token(tokens[5], "epoch.second", line)?;
1952 let civil = validate::civil_datetime_with_second_policy(
1953 i64::from(year),
1954 month,
1955 day,
1956 hour,
1957 minute,
1958 second,
1959 second_policy,
1960 )
1961 .map_err(|error| map_field_error(error, line))?;
1962 let flag = strict_int_token::<u8>(tokens[6], "epoch.flag", line)?;
1963 let numsat = parse_epoch_record_count(tokens[7], line)?;
1964 let clock = field(line, 68, line.len()).trim();
1965 let rcv_clock_offset_s = if clock.is_empty() {
1966 None
1967 } else {
1968 Some(strict_f64_token(clock, "epoch.rcv_clock_offset_s", line)?)
1969 };
1970 Ok((
1971 ObsEpochTime {
1972 year,
1973 month: civil.month as u8,
1974 day: civil.day as u8,
1975 hour: civil.hour as u8,
1976 minute: civil.minute as u8,
1977 second: civil.second,
1978 },
1979 flag,
1980 numsat,
1981 rcv_clock_offset_s,
1982 ))
1983}
1984
1985fn expand_rinex2_year(year: i32) -> i32 {
1986 if year >= 100 {
1987 year
1988 } else if year >= 80 {
1989 1900 + year
1990 } else {
1991 2000 + year
1992 }
1993}
1994
1995fn obs_code_token(token: &str, record: &str, line: &str) -> Result<String> {
2004 if token.len() > OBS_CODE_FIELD_WIDTH {
2005 return Err(Error::Parse(format!(
2006 "RINEX OBS {record} code {token:?} exceeds the A{OBS_CODE_FIELD_WIDTH} field width in {line:?}"
2007 )));
2008 }
2009 Ok(token.to_string())
2010}
2011
2012fn parse_epoch_record_count(token: &str, line: &str) -> Result<usize> {
2013 let count = strict_int_token::<usize>(token, "epoch.satellite_count", line)?;
2014 if token.len() > 3 || count > MAX_EPOCH_RECORD_COUNT {
2015 return Err(Error::Parse(format!(
2016 "RINEX OBS epoch satellite count exceeds the I3 field maximum of {MAX_EPOCH_RECORD_COUNT} in {line:?}"
2017 )));
2018 }
2019 Ok(count)
2020}
2021
2022fn collect_epoch_sv_tokens_v2<'a, I: Iterator<Item = &'a str>>(
2023 first_line: &str,
2024 count: usize,
2025 lines: &mut std::iter::Peekable<I>,
2026) -> Result<Vec<String>> {
2027 let mut tokens = Vec::with_capacity(count);
2028 append_epoch_sv_tokens_v2(first_line, count, &mut tokens);
2029 while tokens.len() < count {
2030 let continuation = lines.next().ok_or_else(|| {
2031 Error::Parse("RINEX OBS v2 epoch truncated: missing satellite-list line".into())
2032 })?;
2033 append_epoch_sv_tokens_v2(
2034 continuation.trim_end_matches(['\r', '\n']),
2035 count,
2036 &mut tokens,
2037 );
2038 }
2039 tokens.truncate(count);
2040 Ok(tokens)
2041}
2042
2043fn append_epoch_sv_tokens_v2(line: &str, count: usize, tokens: &mut Vec<String>) {
2044 let remaining = count.saturating_sub(tokens.len());
2045 for i in 0..remaining.min(12) {
2046 let start = 32 + i * 3;
2047 let token = field(line, start, start + 3);
2048 if token.trim().is_empty() {
2049 break;
2050 }
2051 tokens.push(token.to_string());
2052 }
2053}
2054
2055fn parse_sv_token_v2(token: &str, default_system: GnssSystem) -> Option<GnssSatelliteId> {
2056 let token = token.trim();
2057 if token.is_empty() {
2058 return None;
2059 }
2060 let mut chars = token.chars();
2061 let first = chars.next()?;
2062 let (system, prn_text) = if let Some(system) = GnssSystem::from_letter(first) {
2063 (system, chars.as_str().trim())
2064 } else {
2065 (default_system, token)
2066 };
2067 let prn = prn_text.parse::<u8>().ok()?;
2068 GnssSatelliteId::new(system, prn).ok()
2069}
2070
2071fn canonical_rinex2_obs_code(system: GnssSystem, code: &str) -> String {
2072 let code = code.trim();
2073 if code.len() == 3 {
2074 return code.to_string();
2075 }
2076 let mut chars = code.chars();
2077 let Some(kind) = chars.next() else {
2078 return code.to_string();
2079 };
2080 let Some(band) = chars.next() else {
2081 return code.to_string();
2082 };
2083 if chars.next().is_some() || !matches!(kind, 'C' | 'P' | 'L' | 'D' | 'S') {
2084 return code.to_string();
2085 }
2086
2087 if let Some(mapped) = canonical_rinex2_code_exact(system, kind, band) {
2088 return mapped.to_string();
2089 }
2090
2091 let canonical_kind = if kind == 'P' { 'C' } else { kind };
2092 let attr = rinex2_default_tracking_attr(system, kind, band);
2093 format!("{canonical_kind}{band}{attr}")
2094}
2095
2096fn canonical_rinex2_code_exact(system: GnssSystem, kind: char, band: char) -> Option<&'static str> {
2097 match (system, kind, band) {
2098 (GnssSystem::Gps, 'C', '1') => Some("C1C"),
2099 (GnssSystem::Gps, 'C', '2') => Some("C2C"),
2100 (GnssSystem::Gps, 'P', '1') => Some("C1W"),
2101 (GnssSystem::Gps, 'P', '2') => Some("C2W"),
2102 (GnssSystem::Glonass, 'C', '1') => Some("C1C"),
2103 (GnssSystem::Glonass, 'C', '2') => Some("C2C"),
2104 (GnssSystem::Glonass, 'P', '1') => Some("C1P"),
2105 (GnssSystem::Glonass, 'P', '2') => Some("C2P"),
2106 (GnssSystem::Galileo, 'C', '1') => Some("C1C"),
2107 (GnssSystem::Galileo, 'C', '2') => Some("C5Q"),
2108 (GnssSystem::Galileo, 'P', '1') => Some("C1X"),
2109 (GnssSystem::Galileo, 'P', '2') => Some("C5X"),
2110 (GnssSystem::BeiDou, 'C', '1') => Some("C2I"),
2111 (GnssSystem::BeiDou, 'C', '2') => Some("C7I"),
2112 (GnssSystem::BeiDou, 'P', '1') => Some("C2I"),
2113 (GnssSystem::BeiDou, 'P', '2') => Some("C6I"),
2114 (GnssSystem::Sbas, 'C', '1') => Some("C1C"),
2115 _ => None,
2116 }
2117}
2118
2119fn rinex2_default_tracking_attr(system: GnssSystem, kind: char, band: char) -> char {
2120 match system {
2121 GnssSystem::Gps => match band {
2122 '1' => 'C',
2123 '2' => {
2124 if kind == 'C' {
2125 'C'
2126 } else {
2127 'W'
2128 }
2129 }
2130 '5' => 'X',
2131 _ => 'X',
2132 },
2133 GnssSystem::Glonass => match band {
2134 '1' => 'C',
2135 '2' => 'P',
2136 '3' => 'X',
2137 _ => 'X',
2138 },
2139 GnssSystem::Galileo => match band {
2140 '1' | '6' => 'C',
2141 '5' | '7' | '8' => 'X',
2142 _ => 'X',
2143 },
2144 GnssSystem::BeiDou => match band {
2145 '2' | '6' | '7' => 'I',
2146 '1' => 'P',
2147 '5' | '8' => 'X',
2148 _ => 'X',
2149 },
2150 GnssSystem::Qzss => match band {
2151 '1' => 'C',
2152 '2' => 'L',
2153 '5' | '6' => 'X',
2154 _ => 'X',
2155 },
2156 GnssSystem::Navic => match band {
2157 '5' | '9' => 'A',
2158 _ => 'X',
2159 },
2160 GnssSystem::Sbas => match band {
2161 '1' => 'C',
2162 '5' => 'X',
2163 _ => 'X',
2164 },
2165 }
2166}
2167
2168fn time_scale_from_label(label: &str, line: &str) -> Result<TimeScale> {
2172 let label = label.trim();
2173 if label.is_empty() {
2174 Ok(TimeScale::Gpst)
2175 } else {
2176 time_scale_label(label).ok_or_else(|| {
2177 Error::Parse(format!(
2178 "RINEX OBS TIME OF FIRST OBS unknown time scale {label:?} in {line:?}"
2179 ))
2180 })
2181 }
2182}
2183
2184fn civil_second_policy_for_time_scale(scale: TimeScale) -> validate::CivilSecondPolicy {
2185 match scale {
2186 TimeScale::Utc | TimeScale::Glonasst => validate::CivilSecondPolicy::UtcLike,
2188 TimeScale::Tai
2189 | TimeScale::Tt
2190 | TimeScale::Tcg
2191 | TimeScale::Tdb
2192 | TimeScale::Tcb
2193 | TimeScale::Gpst
2194 | TimeScale::Gst
2195 | TimeScale::Bdt
2196 | TimeScale::Qzsst => validate::CivilSecondPolicy::Continuous,
2197 }
2198}
2199
2200fn parse_epoch_time_tokens(
2201 body: &str,
2202 line: &str,
2203 fields: [&'static str; 6],
2204 second_policy: validate::CivilSecondPolicy,
2205) -> Result<ObsEpochTime> {
2206 let tokens: Vec<&str> = body.split_whitespace().collect();
2207 if tokens.len() < fields.len() {
2208 let field = fields[tokens.len()];
2209 return Err(map_field_error(FieldError::Missing { field }, line));
2210 }
2211 let year = strict_int_token::<i32>(tokens[0], fields[0], line)?;
2212 let month = strict_int_token::<i64>(tokens[1], fields[1], line)?;
2213 let day = strict_int_token::<i64>(tokens[2], fields[2], line)?;
2214 let hour = strict_int_token::<i64>(tokens[3], fields[3], line)?;
2215 let minute = strict_int_token::<i64>(tokens[4], fields[4], line)?;
2216 let second = strict_f64_token(tokens[5], fields[5], line)?;
2217 let civil = validate::civil_datetime_with_second_policy(
2218 year as i64,
2219 month,
2220 day,
2221 hour,
2222 minute,
2223 second,
2224 second_policy,
2225 )
2226 .map_err(|error| map_field_error(error, line))?;
2227 Ok(ObsEpochTime {
2228 year,
2229 month: civil.month as u8,
2230 day: civil.day as u8,
2231 hour: civil.hour as u8,
2232 minute: civil.minute as u8,
2233 second: civil.second,
2234 })
2235}
2236
2237fn strict_vec3_tokens(body: &str, line: &str, fields: [&'static str; 3]) -> Result<[f64; 3]> {
2238 let tokens: Vec<&str> = body.split_whitespace().collect();
2239 if tokens.len() < fields.len() {
2240 let field = fields[tokens.len()];
2241 return Err(map_field_error(FieldError::Missing { field }, line));
2242 }
2243 Ok([
2244 strict_f64_token(tokens[0], fields[0], line)?,
2245 strict_f64_token(tokens[1], fields[1], line)?,
2246 strict_f64_token(tokens[2], fields[2], line)?,
2247 ])
2248}
2249
2250fn optional_f64_field(
2251 line: &str,
2252 start: usize,
2253 end: usize,
2254 field_name: &'static str,
2255) -> Result<Option<f64>> {
2256 let token = field(line, start, end).trim();
2257 if token.is_empty() {
2258 Ok(None)
2259 } else {
2260 strict_f64_token(token, field_name, line).map(Some)
2261 }
2262}
2263
2264fn optional_i64_field(
2265 line: &str,
2266 start: usize,
2267 end: usize,
2268 field_name: &'static str,
2269) -> Result<Option<i64>> {
2270 let token = field(line, start, end).trim();
2271 if token.is_empty() {
2272 Ok(None)
2273 } else {
2274 strict_int_token::<i64>(token, field_name, line).map(Some)
2275 }
2276}
2277
2278fn optional_trimmed(line: &str, start: usize, end: usize) -> Option<String> {
2279 let value = field(line, start, end).trim();
2280 (!value.is_empty()).then(|| value.to_string())
2281}
2282
2283fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
2284where
2285 T: core::str::FromStr,
2286{
2287 strict_int_token(field(line, start, end), field_name, line)
2288}
2289
2290fn strict_f64_token(token: &str, field_name: &'static str, line: &str) -> Result<f64> {
2291 validate::strict_f64(token, field_name).map_err(|error| map_field_error(error, line))
2292}
2293
2294fn validate_finite_input(value: f64, field: &'static str) -> Result<()> {
2295 if value.is_finite() {
2296 Ok(())
2297 } else {
2298 Err(Error::InvalidInput(format!(
2299 "RINEX OBS {field} must be finite"
2300 )))
2301 }
2302}
2303
2304fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
2305where
2306 T: core::str::FromStr,
2307{
2308 validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
2309}
2310
2311fn scale_factor_value(value: u32) -> Result<f64> {
2312 match value {
2313 1 | 10 | 100 | 1000 => Ok(f64::from(value)),
2314 _ => Err(Error::Parse(format!(
2315 "RINEX OBS invalid scale_factor.factor: expected 1, 10, 100, or 1000, got {value}"
2316 ))),
2317 }
2318}
2319
2320fn map_field_error(error: FieldError, line: &str) -> Error {
2321 Error::Parse(format!(
2322 "RINEX OBS invalid {}: {error} in {line:?}",
2323 error.field()
2324 ))
2325}
2326
2327fn obs_payload_field_count(payload_len: usize) -> usize {
2328 let full = payload_len / OBS_FIELD_WIDTH;
2329 let trailing = payload_len % OBS_FIELD_WIDTH;
2330 full + usize::from(trailing >= OBS_VALUE_WIDTH)
2331}
2332
2333fn sat_record_field_count(record_len: usize) -> usize {
2334 obs_payload_field_count(record_len.saturating_sub(3))
2335}
2336
2337fn ascii_fixed_columns(line: &str) -> Cow<'_, str> {
2338 if line.is_ascii() {
2339 Cow::Borrowed(line)
2340 } else {
2341 Cow::Owned(
2342 line.chars()
2343 .map(|ch| if ch.is_ascii() { ch } else { ' ' })
2344 .collect(),
2345 )
2346 }
2347}
2348
2349fn truncate_to_char_boundary(record: &mut String, len: usize) {
2350 let mut end = len.min(record.len());
2351 while !record.is_char_boundary(end) {
2352 end -= 1;
2353 }
2354 record.truncate(end);
2355}
2356
2357fn starts_with_sat_designator(line: &str) -> bool {
2365 let Some(token) = line.get(0..3) else {
2366 return false;
2367 };
2368 let b = token.as_bytes();
2369 let prn = token[1..].trim();
2370 b[0].is_ascii_alphabetic()
2371 && (1..=2).contains(&prn.len())
2372 && prn.bytes().all(|byte| byte.is_ascii_digit())
2373}
2374
2375fn consume_skipped_sat_continuations<'a, I: Iterator<Item = &'a str>>(
2379 lines: &mut std::iter::Peekable<I>,
2380) {
2381 while let Some(raw_next) = lines.peek().copied() {
2382 let next = ascii_fixed_columns(raw_next.trim_end_matches(['\r', '\n']));
2383 if next.starts_with('>') || starts_with_sat_designator(&next) {
2384 break;
2385 }
2386 lines.next();
2387 }
2388}
2389
2390fn append_sat_continuation(record: &mut String, continuation: &str, n_obs: usize) {
2391 let fields_present = sat_record_field_count(record.len());
2392 let logical_len = 3 + fields_present * OBS_FIELD_WIDTH;
2393 truncate_to_char_boundary(record, logical_len);
2394
2395 let remaining = n_obs.saturating_sub(fields_present);
2396 let payload = field(continuation, 3, continuation.len());
2397 let fields_available = obs_payload_field_count(payload.len());
2398 let fields_to_copy = remaining.min(fields_available);
2399 let width = fields_to_copy * OBS_FIELD_WIDTH;
2400 record.push_str(field(payload, 0, width));
2401}
2402
2403fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
2405 token.parse::<GnssSatelliteId>().ok()
2406}
2407
2408fn digit_at(line: &str, col: usize) -> Option<u8> {
2411 line.as_bytes()
2412 .get(col)
2413 .filter(|b| b.is_ascii_digit())
2414 .map(|b| b - b'0')
2415}
2416
2417mod write;
2418
2419#[cfg(all(test, sidereon_repo_tests))]
2420mod tests;