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