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