1use std::borrow::Cow;
39use std::collections::BTreeMap;
40
41use crate::astro::time::model::TimeScale;
42
43use crate::format::columns::{raw_field as field, raw_field_from};
44use crate::format::{Diagnostics, RecordRef, Skip, SkipReason};
45use crate::frequencies::{
46 rinex_band_frequency_hz, rinex_observation_frequency_hz, rinex_observation_wavelength_m,
47};
48use crate::id::{GnssSatelliteId, GnssSystem};
49use crate::rinex_common::time_scale_label;
50use crate::rinex_nav::valid_glonass_frequency_channel;
51use crate::validate::{self, FieldError};
52use crate::{Error, Result};
53
54const OBS_FIELD_WIDTH: usize = 16;
56const OBS_VALUE_WIDTH: usize = 14;
58
59#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct ObsEpochTime {
65 pub year: i32,
67 pub month: u8,
69 pub day: u8,
71 pub hour: u8,
73 pub minute: u8,
75 pub second: f64,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct ObsValue {
83 pub value: Option<f64>,
86 pub lli: Option<u8>,
88 pub ssi: Option<u8>,
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub struct ObsPhaseShift {
95 pub system: GnssSystem,
97 pub code: String,
99 pub correction_cycles: f64,
101 pub satellites: Vec<GnssSatelliteId>,
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub struct ObsScaleFactor {
109 pub system: GnssSystem,
111 pub factor: f64,
113 pub codes: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct PgmRunByDate {
120 pub program: String,
122 pub run_by: String,
124 pub date: String,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct ReceiverInfo {
131 pub number: String,
133 pub receiver_type: String,
135 pub version: String,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct AntennaInfo {
142 pub number: String,
144 pub antenna_type: String,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct ObsLeapSeconds {
151 pub current: i64,
153 pub delta_future: Option<i64>,
155 pub week: Option<i64>,
157 pub day: Option<i64>,
159}
160
161#[derive(Debug, Clone, PartialEq)]
164pub struct ObsEpoch {
165 pub epoch: ObsEpochTime,
167 pub flag: u8,
169 pub rcv_clock_offset_s: Option<f64>,
171 pub epoch_picoseconds: Option<u32>,
173 pub declared_record_count: usize,
175 pub special_record_count: usize,
177 pub sats: BTreeMap<GnssSatelliteId, Vec<ObsValue>>,
180}
181
182#[derive(Debug, Clone, PartialEq)]
184pub struct ObsHeader {
185 pub version: f64,
187 pub approx_position_m: Option<[f64; 3]>,
190 pub antenna_delta_hen_m: Option<[f64; 3]>,
194 pub obs_codes: BTreeMap<GnssSystem, Vec<String>>,
196 pub program_run_by_date: Option<PgmRunByDate>,
198 pub comments: Vec<String>,
200 pub marker_number: Option<String>,
202 pub marker_type: Option<String>,
204 pub observer: Option<String>,
206 pub agency: Option<String>,
208 pub receiver: Option<ReceiverInfo>,
210 pub antenna: Option<AntennaInfo>,
212 pub interval_s: Option<f64>,
214 pub time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
216 pub time_of_last_obs: Option<(ObsEpochTime, TimeScale)>,
218 pub n_satellites: Option<usize>,
220 pub prn_obs_counts: BTreeMap<GnssSatelliteId, Vec<Option<usize>>>,
222 pub phase_shifts: Vec<ObsPhaseShift>,
224 pub scale_factors: Vec<ObsScaleFactor>,
226 pub glonass_slots: BTreeMap<u8, i8>,
228 pub glonass_cod_phs_bis: Option<Vec<(String, f64)>>,
230 pub signal_strength_unit: Option<String>,
232 pub leap_seconds: Option<ObsLeapSeconds>,
234 pub marker_name: Option<String>,
236 pub unretained_header_labels: Vec<String>,
238}
239
240#[derive(Debug, Clone, PartialEq)]
246pub struct RinexObs {
247 pub header: ObsHeader,
249 pub epochs: Vec<ObsEpoch>,
252 pub skipped_records: usize,
259}
260
261impl RinexObs {
262 pub fn parse(text: &str) -> Result<Self> {
268 let mut parser = Parser::new();
269 let mut lines = text.lines();
270 parser.parse_header(&mut lines)?;
271 parser.parse_body(&mut lines.peekable())?;
272 parser.finish()
273 }
274
275 pub fn header(&self) -> &ObsHeader {
277 &self.header
278 }
279
280 pub fn epochs(&self) -> &[ObsEpoch] {
282 &self.epochs
283 }
284
285 pub fn obs_codes(&self, sys: GnssSystem) -> Option<&[String]> {
287 self.header.obs_codes.get(&sys).map(Vec::as_slice)
288 }
289}
290
291impl core::str::FromStr for RinexObs {
292 type Err = Error;
293
294 fn from_str(s: &str) -> Result<Self> {
295 Self::parse(s)
296 }
297}
298
299#[derive(Debug, Clone, PartialEq)]
306pub struct SignalPolicy {
307 pub codes: BTreeMap<GnssSystem, Vec<String>>,
309}
310
311impl SignalPolicy {
312 pub fn default_for(version: f64) -> Result<Self> {
322 validate_finite_input(version, "version")?;
323 let mut codes = BTreeMap::new();
324 codes.insert(GnssSystem::Gps, vec!["C1C".to_string()]);
325 codes.insert(
326 GnssSystem::Galileo,
327 vec!["C1C".to_string(), "C1X".to_string()],
328 );
329 let beidou = if (3.015..3.025).contains(&version) {
334 vec!["C1I".to_string(), "C2I".to_string()]
335 } else {
336 vec!["C2I".to_string(), "C1I".to_string()]
337 };
338 codes.insert(GnssSystem::BeiDou, beidou);
339 codes.insert(GnssSystem::Glonass, vec!["C1C".to_string()]);
340 Ok(Self { codes })
341 }
342
343 pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
345 self.codes.insert(sys, codes);
346 self
347 }
348}
349
350#[derive(Debug, Clone, Default, PartialEq, Eq)]
356pub struct ObservationFilter {
357 pub codes: BTreeMap<GnssSystem, Vec<String>>,
359}
360
361impl ObservationFilter {
362 pub fn all() -> Self {
364 Self::default()
365 }
366
367 pub fn from_entries<I>(entries: I) -> Self
369 where
370 I: IntoIterator<Item = (GnssSystem, Vec<String>)>,
371 {
372 Self {
373 codes: entries.into_iter().collect(),
374 }
375 }
376
377 fn allowed_codes(&self, system: GnssSystem) -> Option<&[String]> {
378 if self.codes.is_empty() {
379 Some(&[])
380 } else {
381 self.codes.get(&system).map(Vec::as_slice)
382 }
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub enum ObservationKind {
389 Pseudorange,
391 CarrierPhase,
393 Doppler,
395 SignalStrength,
397 Unknown,
399}
400
401impl ObservationKind {
402 pub fn from_code(code: &str) -> Self {
404 match code.as_bytes().first().copied() {
405 Some(b'C') => Self::Pseudorange,
406 Some(b'L') => Self::CarrierPhase,
407 Some(b'D') => Self::Doppler,
408 Some(b'S') => Self::SignalStrength,
409 _ => Self::Unknown,
410 }
411 }
412
413 pub fn as_str(self) -> &'static str {
415 match self {
416 Self::Pseudorange => "pseudorange",
417 Self::CarrierPhase => "carrier_phase",
418 Self::Doppler => "doppler",
419 Self::SignalStrength => "signal_strength",
420 Self::Unknown => "unknown",
421 }
422 }
423
424 pub fn units_str(self) -> &'static str {
426 match self {
427 Self::Pseudorange => "meters",
428 Self::CarrierPhase => "cycles",
429 Self::Doppler => "hz",
430 Self::SignalStrength => "db_hz",
431 Self::Unknown => "unknown",
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq)]
438pub struct ObservationValueRow {
439 pub code: String,
441 pub kind: ObservationKind,
443 pub value: Option<f64>,
445 pub lli: Option<u8>,
447 pub ssi: Option<u8>,
449}
450
451#[derive(Debug, Clone, PartialEq)]
453pub struct CarrierPhaseRow {
454 pub code: String,
456 pub value_cycles: Option<f64>,
458 pub lli: Option<u8>,
460 pub ssi: Option<u8>,
462 pub frequency_hz: Option<f64>,
464 pub wavelength_m: Option<f64>,
466 pub value_m: Option<f64>,
468 pub phase_shift_cycles: f64,
472}
473
474pub fn observation_values(
476 obs: &RinexObs,
477 epoch: &ObsEpoch,
478 filter: &ObservationFilter,
479) -> Result<Vec<(GnssSatelliteId, Vec<ObservationValueRow>)>> {
480 let mut out = Vec::new();
481 for (sat, values) in epoch
482 .sats
483 .iter()
484 .filter(|(sat, _)| filter.allowed_codes(sat.system).is_some())
485 {
486 let allowed_codes = filter
487 .allowed_codes(sat.system)
488 .expect("filter presence checked");
489 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
490 continue;
491 };
492 let mut rows = Vec::new();
493 for (code, value) in code_list.iter().zip(values.iter()) {
494 if !allowed_codes.is_empty() && !allowed_codes.iter().any(|c| c == code) {
495 continue;
496 }
497 if let Some(value) = value.value {
498 validate_finite_input(value, "observation.value")?;
499 }
500 let kind = ObservationKind::from_code(code);
501 rows.push(ObservationValueRow {
502 code: code.clone(),
503 kind,
504 value: value.value,
505 lli: value.lli,
506 ssi: value.ssi,
507 });
508 }
509 out.push((*sat, rows));
510 }
511 Ok(out)
512}
513
514pub fn carrier_phase_rows(
516 obs: &RinexObs,
517 epoch: &ObsEpoch,
518 filter: &ObservationFilter,
519) -> Result<Vec<(GnssSatelliteId, Vec<CarrierPhaseRow>)>> {
520 validate_finite_input(obs.header.version, "version")?;
521 let mut out = Vec::new();
522 for (sat, rows) in observation_values(obs, epoch, filter)? {
523 let phases = rows
524 .into_iter()
525 .filter(|row| row.kind == ObservationKind::CarrierPhase)
526 .map(|row| carrier_phase_row(obs, sat, row))
527 .collect::<Result<Vec<_>>>()?;
528 out.push((sat, phases));
529 }
530 Ok(out)
531}
532
533pub fn band_frequency_hz(
538 system: GnssSystem,
539 band: char,
540 glonass_channel: Option<i8>,
541) -> Option<f64> {
542 rinex_band_frequency_hz(system, band, glonass_channel)
543}
544
545pub fn observation_frequency_hz(
547 system: GnssSystem,
548 code: &str,
549 rinex_version: f64,
550 glonass_channel: Option<i8>,
551) -> Result<Option<f64>> {
552 validate_finite_input(rinex_version, "version")?;
553 Ok(rinex_observation_frequency_hz(
554 system,
555 code,
556 rinex_version,
557 glonass_channel,
558 ))
559}
560
561fn carrier_phase_row(
562 obs: &RinexObs,
563 sat: GnssSatelliteId,
564 row: ObservationValueRow,
565) -> Result<CarrierPhaseRow> {
566 let glonass_channel = obs.header.glonass_slots.get(&sat.prn).copied();
567 let frequency_hz =
568 observation_frequency_hz(sat.system, &row.code, obs.header.version, glonass_channel)?;
569 let phase_shift_cycles = phase_shift_cycles(obs, sat, &row.code);
570 let value_cycles = row.value;
571 let wavelength_m =
572 rinex_observation_wavelength_m(sat.system, &row.code, obs.header.version, glonass_channel);
573 let value_m = match value_cycles.zip(wavelength_m) {
574 Some((cycles, lambda)) => {
575 let value_m = cycles * lambda;
576 validate_finite_input(value_m, "carrier_phase.value_m")?;
577 Some(value_m)
578 }
579 None => None,
580 };
581 Ok(CarrierPhaseRow {
582 code: row.code,
583 value_cycles,
584 lli: row.lli,
585 ssi: row.ssi,
586 frequency_hz,
587 wavelength_m,
588 value_m,
589 phase_shift_cycles,
590 })
591}
592
593fn phase_shift_cycles(obs: &RinexObs, sat: GnssSatelliteId, code: &str) -> f64 {
594 let mut system_wide = None;
595 for shift in obs.header.phase_shifts.iter().rev() {
596 if shift.system != sat.system || shift.code != code {
597 continue;
598 }
599 if shift.satellites.is_empty() {
600 if system_wide.is_none() {
601 system_wide = Some(shift.correction_cycles);
602 }
603 } else if shift.satellites.contains(&sat) {
604 return shift.correction_cycles;
605 }
606 }
607 system_wide.unwrap_or(0.0)
608}
609
610pub fn pseudoranges(
617 obs: &RinexObs,
618 epoch: &ObsEpoch,
619 policy: &SignalPolicy,
620) -> Result<Vec<(GnssSatelliteId, f64)>> {
621 let mut out = Vec::new();
622 for (sat, values) in &epoch.sats {
623 let Some(prefs) = policy.codes.get(&sat.system) else {
624 continue;
625 };
626 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
627 continue;
628 };
629 for code in prefs {
630 if let Some(idx) = code_list.iter().position(|c| c == code) {
631 if let Some(ObsValue {
632 value: Some(range_m),
633 ..
634 }) = values.get(idx)
635 {
636 validate_finite_input(*range_m, "pseudorange_m")?;
637 out.push((*sat, *range_m));
638 break;
639 }
640 }
641 }
642 }
643 Ok(out)
644}
645
646struct Parser {
648 version: Option<f64>,
649 is_observation: bool,
650 approx_position_m: Option<[f64; 3]>,
651 antenna_delta_hen_m: Option<[f64; 3]>,
652 obs_codes: BTreeMap<GnssSystem, Vec<String>>,
653 interval_s: Option<f64>,
654 time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
655 time_of_last_obs: Option<(ObsEpochTime, TimeScale)>,
656 program_run_by_date: Option<PgmRunByDate>,
657 comments: Vec<String>,
658 marker_number: Option<String>,
659 marker_type: Option<String>,
660 observer: Option<String>,
661 agency: Option<String>,
662 receiver: Option<ReceiverInfo>,
663 antenna: Option<AntennaInfo>,
664 n_satellites: Option<usize>,
665 prn_obs_counts: BTreeMap<GnssSatelliteId, Vec<Option<usize>>>,
666 phase_shifts: Vec<ObsPhaseShift>,
667 scale_factors: Vec<ObsScaleFactor>,
668 scale_factor_continuation: Option<ScaleFactorContinuation>,
669 glonass_slots: BTreeMap<u8, i8>,
670 glonass_slots_remaining: Option<usize>,
671 glonass_cod_phs_bis: Option<Vec<(String, f64)>>,
672 signal_strength_unit: Option<String>,
673 leap_seconds: Option<ObsLeapSeconds>,
674 marker_name: Option<String>,
675 unretained_header_labels: Vec<String>,
676 epochs: Vec<ObsEpoch>,
677 current_obs_sys: Option<GnssSystem>,
680 obs_codes_remaining: usize,
682 diagnostics: Diagnostics,
687}
688
689#[derive(Debug, Clone, Copy)]
690struct ScaleFactorContinuation {
691 remaining: usize,
692}
693
694impl Parser {
695 fn new() -> Self {
696 Self {
697 version: None,
698 is_observation: false,
699 approx_position_m: None,
700 antenna_delta_hen_m: None,
701 obs_codes: BTreeMap::new(),
702 interval_s: None,
703 time_of_first_obs: None,
704 time_of_last_obs: None,
705 program_run_by_date: None,
706 comments: Vec::new(),
707 marker_number: None,
708 marker_type: None,
709 observer: None,
710 agency: None,
711 receiver: None,
712 antenna: None,
713 n_satellites: None,
714 prn_obs_counts: BTreeMap::new(),
715 phase_shifts: Vec::new(),
716 scale_factors: Vec::new(),
717 scale_factor_continuation: None,
718 glonass_slots: BTreeMap::new(),
719 glonass_slots_remaining: None,
720 glonass_cod_phs_bis: None,
721 signal_strength_unit: None,
722 leap_seconds: None,
723 marker_name: None,
724 unretained_header_labels: Vec::new(),
725 epochs: Vec::new(),
726 current_obs_sys: None,
727 obs_codes_remaining: 0,
728 diagnostics: Diagnostics::new(),
729 }
730 }
731
732 fn push_unrepresentable_satellite_skip(&mut self, token: &str) {
735 self.diagnostics.push_skip(Skip {
736 at: RecordRef::default().with_satellite(token.trim()),
737 reason: SkipReason::UnrepresentableSatellite,
738 });
739 }
740
741 fn parse_header<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut I) -> Result<()> {
742 let mut saw_end = false;
743 for raw in lines.by_ref() {
744 let line = raw.trim_end_matches(['\r', '\n']);
745 let label = raw_field_from(line, 60).trim();
746 match label {
747 "RINEX VERSION / TYPE" => self.parse_version(line)?,
748 "PGM / RUN BY / DATE" => self.parse_pgm_run_by_date(line),
749 "COMMENT" => self.comments.push(field(line, 0, 60).trim().to_string()),
750 "APPROX POSITION XYZ" => self.parse_approx_position(line)?,
751 "ANTENNA: DELTA H/E/N" => self.parse_antenna_delta(line)?,
752 "SYS / # / OBS TYPES" => self.parse_obs_types(line)?,
753 "SYS / SCALE FACTOR" => self.parse_scale_factor(line)?,
754 "SYS / PHASE SHIFT" => self.parse_phase_shift(line)?,
755 "TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
756 "TIME OF LAST OBS" => self.parse_time_of_last_obs(line)?,
757 "INTERVAL" => {
758 self.interval_s = Some(strict_f64_field(line, 0, 10, "interval_s")?);
759 }
760 "GLONASS SLOT / FRQ #" => self.parse_glonass_slots(line)?,
761 "GLONASS COD/PHS/BIS" => self.parse_glonass_cod_phs_bis(line)?,
762 "SIGNAL STRENGTH UNIT" => {
763 let unit = field(line, 0, 20).trim();
764 if !unit.is_empty() {
765 self.signal_strength_unit = Some(unit.to_string());
766 }
767 }
768 "LEAP SECONDS" => self.parse_leap_seconds(line)?,
769 "# OF SATELLITES" => {
770 self.n_satellites =
771 Some(strict_int_field::<usize>(line, 0, 6, "n_satellites")?);
772 }
773 "PRN / # OF OBS" => self.parse_prn_obs_counts(line)?,
774 "MARKER NAME" => {
775 let name = field(line, 0, 60).trim();
776 if !name.is_empty() {
777 self.marker_name = Some(name.to_string());
778 }
779 }
780 "MARKER NUMBER" => {
781 self.marker_number = optional_trimmed(line, 0, 20);
782 }
783 "MARKER TYPE" => {
784 self.marker_type = optional_trimmed(line, 0, 20);
785 }
786 "OBSERVER / AGENCY" => {
787 self.observer = optional_trimmed(line, 0, 20);
788 self.agency = optional_trimmed(line, 20, 60);
789 }
790 "REC # / TYPE / VERS" => {
791 self.receiver = Some(ReceiverInfo {
792 number: field(line, 0, 20).trim().to_string(),
793 receiver_type: field(line, 20, 40).trim().to_string(),
794 version: field(line, 40, 60).trim().to_string(),
795 });
796 }
797 "ANT # / TYPE" => {
798 self.antenna = Some(AntennaInfo {
799 number: field(line, 0, 20).trim().to_string(),
800 antenna_type: field(line, 20, 40).trim().to_string(),
801 });
802 }
803 "END OF HEADER" => {
804 self.ensure_obs_type_count_complete(line)?;
805 self.ensure_scale_factor_count_complete(line)?;
806 saw_end = true;
807 break;
808 }
809 _ => {
812 if !label.is_empty() {
813 self.unretained_header_labels.push(label.to_string());
814 }
815 }
816 }
817 }
818 if !saw_end {
819 return Err(Error::Parse("RINEX OBS header has no END OF HEADER".into()));
820 }
821 Ok(())
822 }
823
824 fn parse_version(&mut self, line: &str) -> Result<()> {
825 let version = field(line, 0, 20).trim();
826 let version = strict_f64_token(version, "version", line)?;
827 let type_field = field(line, 20, 40);
829 self.is_observation =
830 type_field.trim_start().starts_with('O') || type_field.contains("OBSERVATION");
831 if !self.is_observation {
832 return Err(Error::Parse(format!(
833 "RINEX file is not observation data: {type_field:?}"
834 )));
835 }
836 if !matches!(version.floor() as i64, 3 | 4) {
837 return Err(Error::Parse(format!(
838 "RINEX OBS parser requires major version 3 or 4, got {version}"
839 )));
840 }
841 self.version = Some(version);
842 Ok(())
843 }
844
845 fn parse_approx_position(&mut self, line: &str) -> Result<()> {
846 let body = field(line, 0, 60);
847 self.approx_position_m = Some(strict_vec3_tokens(
848 body,
849 line,
850 [
851 "approx_position.x_m",
852 "approx_position.y_m",
853 "approx_position.z_m",
854 ],
855 )?);
856 Ok(())
857 }
858
859 fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
860 let body = field(line, 0, 60);
861 self.antenna_delta_hen_m = Some(strict_vec3_tokens(
862 body,
863 line,
864 [
865 "antenna_delta.height_m",
866 "antenna_delta.east_m",
867 "antenna_delta.north_m",
868 ],
869 )?);
870 Ok(())
871 }
872
873 fn parse_pgm_run_by_date(&mut self, line: &str) {
874 self.program_run_by_date = Some(PgmRunByDate {
875 program: field(line, 0, 20).trim().to_string(),
876 run_by: field(line, 20, 40).trim().to_string(),
877 date: field(line, 40, 60).trim().to_string(),
878 });
879 }
880
881 fn parse_obs_types(&mut self, line: &str) -> Result<()> {
882 let sys_field = field(line, 0, 1).trim();
886 if !sys_field.is_empty() {
887 self.ensure_obs_type_count_complete(line)?;
888 let letter = sys_field.chars().next().unwrap();
889 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
890 Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
891 })?;
892 let count = strict_int_field::<usize>(line, 3, 6, "obs_type_count")?;
893 self.current_obs_sys = Some(system);
894 self.obs_codes_remaining = count;
895 self.obs_codes.entry(system).or_default();
896 }
897 let Some(system) = self.current_obs_sys else {
898 return Ok(());
899 };
900 let codes_section = field(line, 7, 60);
903 let list = self.obs_codes.get_mut(&system).expect("system inserted");
904 for tok in codes_section.split_whitespace() {
905 if self.obs_codes_remaining == 0 {
906 return Err(Error::Parse(format!(
907 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
908 )));
909 }
910 list.push(tok.to_string());
911 self.obs_codes_remaining -= 1;
912 }
913 Ok(())
914 }
915
916 fn ensure_obs_type_count_complete(&self, line: &str) -> Result<()> {
917 if self.obs_codes_remaining == 0 {
918 return Ok(());
919 }
920 let Some(system) = self.current_obs_sys else {
921 return Ok(());
922 };
923 let supplied = self.obs_codes.get(&system).map_or(0, Vec::len);
924 let declared = supplied + self.obs_codes_remaining;
925 Err(Error::Parse(format!(
926 "RINEX OBS {system} SYS / # / OBS TYPES declares {declared} codes but supplies {supplied} before {line:?}"
927 )))
928 }
929
930 fn parse_phase_shift(&mut self, line: &str) -> Result<()> {
931 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
932 if tokens.is_empty() {
933 return Ok(());
934 }
935 if tokens.len() < 2 {
936 return Err(Error::Parse(format!(
937 "RINEX OBS phase-shift header has too few fields in {line:?}"
938 )));
939 }
940
941 let system = tokens[0]
942 .chars()
943 .next()
944 .and_then(GnssSystem::from_letter)
945 .ok_or_else(|| {
946 Error::Parse(format!(
947 "RINEX OBS phase-shift system unparsable in {line:?}"
948 ))
949 })?;
950 let code = tokens[1].to_string();
951 let correction_cycles = match tokens.get(2) {
952 Some(token) => strict_f64_token(token, "phase_shift.correction_cycles", line)?,
953 None => 0.0,
954 };
955
956 let satellites = if let Some(count_token) = tokens.get(3) {
957 let count =
958 strict_int_token::<usize>(count_token, "phase_shift.satellite_count", line)?;
959 let sat_tokens = &tokens[4..];
960 if sat_tokens.len() != count {
961 return Err(Error::Parse(format!(
962 "RINEX OBS phase-shift satellite count mismatch in {line:?}"
963 )));
964 }
965 sat_tokens
966 .iter()
967 .map(|token| {
968 parse_sv_token(token).ok_or_else(|| {
969 Error::Parse(format!(
970 "RINEX OBS phase-shift satellite token {token:?} unparsable in {line:?}"
971 ))
972 })
973 })
974 .collect::<Result<Vec<_>>>()?
975 } else {
976 Vec::new()
977 };
978
979 self.phase_shifts.push(ObsPhaseShift {
980 system,
981 code,
982 correction_cycles,
983 satellites,
984 });
985 Ok(())
986 }
987
988 fn parse_scale_factor(&mut self, line: &str) -> Result<()> {
989 let sys_field = field(line, 0, 1).trim();
990 if !sys_field.is_empty() {
991 self.ensure_scale_factor_count_complete(line)?;
992 let letter = sys_field.chars().next().unwrap();
993 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
994 Error::Parse(format!("RINEX OBS unknown scale-factor system {letter:?}"))
995 })?;
996 let factor =
997 scale_factor_value(strict_int_field::<u32>(line, 2, 6, "scale_factor.factor")?)?;
998 let count_field = field(line, 8, 10).trim();
999 let count = if count_field.is_empty() {
1000 0
1001 } else {
1002 strict_int_token::<usize>(count_field, "scale_factor.obs_type_count", line)?
1003 };
1004 self.scale_factors.push(ObsScaleFactor {
1005 system,
1006 factor,
1007 codes: Vec::new(),
1008 });
1009 if count == 0 {
1010 return Ok(());
1011 }
1012 self.scale_factor_continuation = Some(ScaleFactorContinuation { remaining: count });
1013 }
1014
1015 self.collect_scale_factor_codes(line)
1016 }
1017
1018 fn collect_scale_factor_codes(&mut self, line: &str) -> Result<()> {
1019 let Some(mut continuation) = self.scale_factor_continuation else {
1020 return Ok(());
1021 };
1022 let record = self
1023 .scale_factors
1024 .last_mut()
1025 .expect("scale factor continuation has a record");
1026 for code in field(line, 10, 60).split_whitespace() {
1027 if continuation.remaining == 0 {
1028 return Err(Error::Parse(format!(
1029 "RINEX OBS SYS / SCALE FACTOR lists more codes than declared in {line:?}"
1030 )));
1031 }
1032 record.codes.push(code.to_string());
1033 continuation.remaining -= 1;
1034 }
1035 self.scale_factor_continuation = (continuation.remaining > 0).then_some(continuation);
1036 Ok(())
1037 }
1038
1039 fn ensure_scale_factor_count_complete(&self, line: &str) -> Result<()> {
1040 let Some(continuation) = self.scale_factor_continuation else {
1041 return Ok(());
1042 };
1043 let supplied = self
1044 .scale_factors
1045 .last()
1046 .map_or(0, |record| record.codes.len());
1047 let declared = supplied + continuation.remaining;
1048 Err(Error::Parse(format!(
1049 "RINEX OBS SYS / SCALE FACTOR declares {declared} codes but supplies {supplied} before {line:?}"
1050 )))
1051 }
1052
1053 fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
1054 self.time_of_first_obs = Some(self.parse_time_header(line, "time_of_first_obs")?);
1055 Ok(())
1056 }
1057
1058 fn parse_time_of_last_obs(&mut self, line: &str) -> Result<()> {
1059 self.time_of_last_obs = Some(self.parse_time_header(line, "time_of_last_obs")?);
1060 Ok(())
1061 }
1062
1063 fn parse_time_header(
1064 &self,
1065 line: &str,
1066 prefix: &'static str,
1067 ) -> Result<(ObsEpochTime, TimeScale)> {
1068 let body = field(line, 0, 43);
1069 let scale_label = field(line, 48, 51).trim();
1070 let scale = time_scale_from_label(scale_label, line)?;
1071 let year = match prefix {
1072 "time_of_last_obs" => "time_of_last_obs.year",
1073 _ => "time_of_first_obs.year",
1074 };
1075 let month = match prefix {
1076 "time_of_last_obs" => "time_of_last_obs.month",
1077 _ => "time_of_first_obs.month",
1078 };
1079 let day = match prefix {
1080 "time_of_last_obs" => "time_of_last_obs.day",
1081 _ => "time_of_first_obs.day",
1082 };
1083 let hour = match prefix {
1084 "time_of_last_obs" => "time_of_last_obs.hour",
1085 _ => "time_of_first_obs.hour",
1086 };
1087 let minute = match prefix {
1088 "time_of_last_obs" => "time_of_last_obs.minute",
1089 _ => "time_of_first_obs.minute",
1090 };
1091 let second = match prefix {
1092 "time_of_last_obs" => "time_of_last_obs.second",
1093 _ => "time_of_first_obs.second",
1094 };
1095 let epoch = parse_epoch_time_tokens(
1096 body,
1097 line,
1098 [year, month, day, hour, minute, second],
1099 civil_second_policy_for_time_scale(scale),
1100 )?;
1101 Ok((epoch, scale))
1102 }
1103
1104 fn parse_glonass_slots(&mut self, line: &str) -> Result<()> {
1105 let count_field = field(line, 0, 3).trim();
1107 if !count_field.is_empty() {
1108 let count = strict_int_token::<usize>(count_field, "glonass_slot.count", line)?;
1109 self.glonass_slots_remaining = Some(count);
1110 }
1111 let body = field(line, 4, 60);
1112 let tokens: Vec<&str> = body.split_whitespace().collect();
1113 if !tokens.len().is_multiple_of(2) {
1114 return Err(Error::Parse(format!(
1115 "RINEX OBS GLONASS slot table has an odd token count in {line:?}"
1116 )));
1117 }
1118 for pair in tokens.chunks_exact(2) {
1119 if let Some(remaining) = self.glonass_slots_remaining.as_mut() {
1123 if *remaining == 0 {
1124 return Err(Error::Parse(format!(
1125 "RINEX OBS GLONASS slot table has more entries than declared in {line:?}"
1126 )));
1127 }
1128 *remaining -= 1;
1129 }
1130 let Some(sat) = parse_sv_token(pair[0]) else {
1136 self.push_unrepresentable_satellite_skip(pair[0]);
1137 continue;
1138 };
1139 if sat.system != GnssSystem::Glonass {
1140 return Err(Error::Parse(format!(
1141 "RINEX OBS GLONASS slot token {:?} is not GLONASS in {line:?}",
1142 pair[0]
1143 )));
1144 }
1145 let channel = strict_int_token::<i8>(pair[1], "glonass_slot.channel", line)?;
1146 if !valid_glonass_frequency_channel(i32::from(channel)) {
1147 return Err(Error::Parse(format!(
1148 "RINEX OBS invalid glonass_slot.channel: {channel} out of range in {line:?}"
1149 )));
1150 }
1151 self.glonass_slots.insert(sat.prn, channel);
1152 }
1153 Ok(())
1154 }
1155
1156 fn parse_glonass_cod_phs_bis(&mut self, line: &str) -> Result<()> {
1157 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
1158 let mut entries = Vec::new();
1159 for pair in tokens.chunks(2) {
1160 if pair.len() != 2 {
1161 return Err(Error::Parse(format!(
1162 "RINEX OBS GLONASS COD/PHS/BIS has an odd token count in {line:?}"
1163 )));
1164 }
1165 entries.push((
1166 pair[0].to_string(),
1167 strict_f64_token(pair[1], "glonass_code_phase_bias", line)?,
1168 ));
1169 }
1170 self.glonass_cod_phs_bis = Some(entries);
1171 Ok(())
1172 }
1173
1174 fn parse_leap_seconds(&mut self, line: &str) -> Result<()> {
1175 let current = strict_int_field::<i64>(line, 0, 6, "leap_seconds.current")?;
1176 self.leap_seconds = Some(ObsLeapSeconds {
1177 current,
1178 delta_future: optional_i64_field(line, 6, 12, "leap_seconds.delta_future")?,
1179 week: optional_i64_field(line, 12, 18, "leap_seconds.week")?,
1180 day: optional_i64_field(line, 18, 24, "leap_seconds.day")?,
1181 });
1182 Ok(())
1183 }
1184
1185 fn parse_prn_obs_counts(&mut self, line: &str) -> Result<()> {
1186 let token = field(line, 0, 3).trim();
1187 if token.is_empty() {
1188 return Ok(());
1189 }
1190 let Some(sat) = parse_sv_token(token) else {
1191 self.push_unrepresentable_satellite_skip(token);
1192 return Ok(());
1193 };
1194 let count = self.obs_codes.get(&sat.system).map_or(0, Vec::len);
1195 let mut values = Vec::with_capacity(count);
1196 for idx in 0..count {
1197 let start = 3 + idx * 6;
1198 let raw = field(line, start, start + 6).trim();
1199 if raw.is_empty() {
1200 values.push(None);
1201 } else {
1202 values.push(Some(strict_int_token::<usize>(raw, "prn_obs_count", line)?));
1203 }
1204 }
1205 self.prn_obs_counts.insert(sat, values);
1206 Ok(())
1207 }
1208
1209 fn parse_body<'a, I: Iterator<Item = &'a str>>(
1210 &mut self,
1211 lines: &mut std::iter::Peekable<I>,
1212 ) -> Result<()> {
1213 while let Some(raw) = lines.next() {
1214 let line = raw.trim_end_matches(['\r', '\n']);
1215 if line.is_empty() {
1216 continue;
1217 }
1218 if !line.starts_with('>') {
1219 continue;
1221 }
1222 let time_scale = self
1223 .time_of_first_obs
1224 .map_or(TimeScale::Gpst, |(_, scale)| scale);
1225 let (epoch_time, flag, numsat, rcv_clock_offset_s, epoch_picoseconds) =
1226 parse_epoch_line(line, civil_second_policy_for_time_scale(time_scale))?;
1227
1228 if flag > 1 {
1229 for _ in 0..numsat {
1233 lines
1234 .next()
1235 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
1236 }
1237 self.epochs.push(ObsEpoch {
1238 epoch: epoch_time,
1239 flag,
1240 rcv_clock_offset_s,
1241 epoch_picoseconds,
1242 declared_record_count: numsat,
1243 special_record_count: numsat,
1244 sats: BTreeMap::new(),
1245 });
1246 continue;
1247 }
1248
1249 let mut sats = BTreeMap::new();
1250 for _ in 0..numsat {
1251 let sat_line = lines.next().ok_or_else(|| {
1252 Error::Parse("RINEX OBS epoch truncated: missing satellite line".into())
1253 })?;
1254 let sat_line = sat_line.trim_end_matches(['\r', '\n']);
1255 let normalized = ascii_fixed_columns(sat_line);
1262 if !starts_with_sat_designator(&normalized) {
1263 return Err(Error::Parse(
1268 "RINEX OBS epoch truncated: expected satellite record".into(),
1269 ));
1270 }
1271 if parse_sv_token(field(&normalized, 0, 3)).is_none() {
1272 self.push_unrepresentable_satellite_skip(field(&normalized, 0, 3));
1277 consume_skipped_sat_continuations(lines);
1278 continue;
1279 }
1280 let sat_record = self.collect_sat_record(sat_line, lines)?;
1281 let (sat, values) = self.parse_sat_line(&sat_record)?;
1282 sats.insert(sat, values);
1283 }
1284 self.epochs.push(ObsEpoch {
1285 epoch: epoch_time,
1286 flag,
1287 rcv_clock_offset_s,
1288 epoch_picoseconds,
1289 declared_record_count: numsat,
1290 special_record_count: 0,
1291 sats,
1292 });
1293 }
1294 Ok(())
1295 }
1296
1297 fn collect_sat_record<'a, I: Iterator<Item = &'a str>>(
1298 &self,
1299 first_line: &str,
1300 lines: &mut std::iter::Peekable<I>,
1301 ) -> Result<String> {
1302 let first_line = ascii_fixed_columns(first_line);
1303 let token = field(&first_line, 0, 3);
1304 let sat = parse_sv_token(token).ok_or_else(|| {
1305 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1306 })?;
1307 let n_obs = self.obs_count_for_sat(sat)?;
1308 let mut record = first_line.into_owned();
1309
1310 while sat_record_field_count(record.len()) < n_obs {
1311 let Some(raw_next) = lines.peek().copied() else {
1312 break;
1313 };
1314 let next = raw_next.trim_end_matches(['\r', '\n']);
1315 let next = ascii_fixed_columns(next);
1316 if next.starts_with('>') || starts_with_sat_designator(&next) {
1323 break;
1324 }
1325 let continuation = lines.next().expect("peeked continuation line");
1326 let continuation = ascii_fixed_columns(continuation.trim_end_matches(['\r', '\n']));
1327 append_sat_continuation(&mut record, &continuation, n_obs);
1328 }
1329
1330 Ok(record)
1331 }
1332
1333 fn obs_count_for_sat(&self, sat: GnssSatelliteId) -> Result<usize> {
1334 self.obs_codes
1335 .get(&sat.system)
1336 .map(Vec::len)
1337 .ok_or_else(|| {
1338 Error::Parse(format!(
1339 "RINEX OBS satellite {sat} uses undeclared observation system"
1340 ))
1341 })
1342 }
1343
1344 fn parse_sat_line(&self, line: &str) -> Result<(GnssSatelliteId, Vec<ObsValue>)> {
1345 let token = field(line, 0, 3);
1346 let sat = parse_sv_token(token).ok_or_else(|| {
1347 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1348 })?;
1349 let code_list = self.obs_codes.get(&sat.system).ok_or_else(|| {
1350 Error::Parse(format!(
1351 "RINEX OBS satellite {sat} uses undeclared observation system"
1352 ))
1353 })?;
1354 let mut values = Vec::with_capacity(code_list.len());
1355 for (i, code) in code_list.iter().enumerate() {
1356 let start = 3 + i * OBS_FIELD_WIDTH;
1357 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1358 let value = if value_str.is_empty() {
1359 None
1360 } else {
1361 let scale = self.scale_factor_for(sat.system, code);
1362 let parsed = strict_f64_token(value_str, "observation.value", line)? / scale;
1363 if format!("{:.3}", parsed * scale).len() > OBS_VALUE_WIDTH {
1370 return Err(Error::Parse(
1371 "RINEX OBS observation value exceeds the F14.3 field width".into(),
1372 ));
1373 }
1374 Some(parsed)
1375 };
1376 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1377 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1378 values.push(ObsValue { value, lli, ssi });
1379 }
1380 Ok((sat, values))
1381 }
1382
1383 fn finish(self) -> Result<RinexObs> {
1384 let version = self
1385 .version
1386 .ok_or_else(|| Error::Parse("RINEX OBS missing RINEX VERSION / TYPE".into()))?;
1387 if let Some(remaining) = self.glonass_slots_remaining {
1388 if remaining != 0 {
1389 return Err(Error::Parse(format!(
1390 "RINEX OBS GLONASS slot table missing {remaining} declared entries"
1391 )));
1392 }
1393 }
1394 if self.obs_codes.is_empty() {
1395 return Err(Error::Parse(
1396 "RINEX OBS header has no SYS / # / OBS TYPES records".into(),
1397 ));
1398 }
1399 let header = ObsHeader {
1400 version,
1401 approx_position_m: self.approx_position_m,
1402 antenna_delta_hen_m: self.antenna_delta_hen_m,
1403 obs_codes: self.obs_codes,
1404 program_run_by_date: self.program_run_by_date,
1405 comments: self.comments,
1406 marker_number: self.marker_number,
1407 marker_type: self.marker_type,
1408 observer: self.observer,
1409 agency: self.agency,
1410 receiver: self.receiver,
1411 antenna: self.antenna,
1412 interval_s: self.interval_s,
1413 time_of_first_obs: self.time_of_first_obs,
1414 time_of_last_obs: self.time_of_last_obs,
1415 n_satellites: self.n_satellites,
1416 prn_obs_counts: self.prn_obs_counts,
1417 phase_shifts: self.phase_shifts,
1418 scale_factors: self.scale_factors,
1419 glonass_slots: self.glonass_slots,
1420 glonass_cod_phs_bis: self.glonass_cod_phs_bis,
1421 signal_strength_unit: self.signal_strength_unit,
1422 leap_seconds: self.leap_seconds,
1423 marker_name: self.marker_name,
1424 unretained_header_labels: self.unretained_header_labels,
1425 };
1426 Ok(RinexObs {
1427 header,
1428 epochs: self.epochs,
1429 skipped_records: self.diagnostics.skips.len(),
1430 })
1431 }
1432
1433 fn scale_factor_for(&self, system: GnssSystem, code: &str) -> f64 {
1434 self.scale_factors
1435 .iter()
1436 .rev()
1437 .find(|record| {
1438 record.system == system
1439 && (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
1440 })
1441 .map_or(1.0, |record| record.factor)
1442 }
1443}
1444
1445type ParsedEpochLine = (ObsEpochTime, u8, usize, Option<f64>, Option<u32>);
1448
1449fn parse_epoch_line(
1450 line: &str,
1451 second_policy: validate::CivilSecondPolicy,
1452) -> Result<ParsedEpochLine> {
1453 let body = line
1454 .strip_prefix('>')
1455 .ok_or_else(|| Error::Parse(format!("RINEX OBS epoch line lacks '>': {line:?}")))?;
1456 let tokens: Vec<&str> = body.split_whitespace().collect();
1457 if tokens.len() < 8 {
1458 return Err(Error::Parse(format!(
1459 "RINEX OBS epoch line has too few fields in {line:?}"
1460 )));
1461 }
1462 let epoch = parse_epoch_time_tokens(
1463 &tokens[..6].join(" "),
1464 line,
1465 [
1466 "epoch.year",
1467 "epoch.month",
1468 "epoch.day",
1469 "epoch.hour",
1470 "epoch.minute",
1471 "epoch.second",
1472 ],
1473 second_policy,
1474 )?;
1475
1476 let mut index = 6;
1477 let epoch_picoseconds = if tokens
1478 .get(index)
1479 .is_some_and(|token| token.len() == 5 && token.bytes().all(|b| b.is_ascii_digit()))
1480 && tokens.len() >= 9
1481 {
1482 let value = strict_int_token::<u32>(tokens[index], "epoch.picoseconds", line)?;
1483 index += 1;
1484 Some(value)
1485 } else {
1486 None
1487 };
1488 let flag = strict_int_token::<u8>(tokens[index], "epoch.flag", line)?;
1489 index += 1;
1490 let numsat = strict_int_token::<usize>(tokens[index], "epoch.satellite_count", line)?;
1491 index += 1;
1492 let rcv_clock_offset_s = tokens
1493 .get(index)
1494 .map(|token| strict_f64_token(token, "epoch.rcv_clock_offset_s", line))
1495 .transpose()?;
1496 Ok((epoch, flag, numsat, rcv_clock_offset_s, epoch_picoseconds))
1497}
1498
1499fn time_scale_from_label(label: &str, line: &str) -> Result<TimeScale> {
1503 let label = label.trim();
1504 if label.is_empty() {
1505 Ok(TimeScale::Gpst)
1506 } else {
1507 time_scale_label(label).ok_or_else(|| {
1508 Error::Parse(format!(
1509 "RINEX OBS TIME OF FIRST OBS unknown time scale {label:?} in {line:?}"
1510 ))
1511 })
1512 }
1513}
1514
1515fn civil_second_policy_for_time_scale(scale: TimeScale) -> validate::CivilSecondPolicy {
1516 match scale {
1517 TimeScale::Utc | TimeScale::Glonasst => validate::CivilSecondPolicy::UtcLike,
1519 TimeScale::Tai
1520 | TimeScale::Tt
1521 | TimeScale::Tdb
1522 | TimeScale::Gpst
1523 | TimeScale::Gst
1524 | TimeScale::Bdt
1525 | TimeScale::Qzsst => validate::CivilSecondPolicy::Continuous,
1526 }
1527}
1528
1529fn parse_epoch_time_tokens(
1530 body: &str,
1531 line: &str,
1532 fields: [&'static str; 6],
1533 second_policy: validate::CivilSecondPolicy,
1534) -> Result<ObsEpochTime> {
1535 let tokens: Vec<&str> = body.split_whitespace().collect();
1536 if tokens.len() < fields.len() {
1537 let field = fields[tokens.len()];
1538 return Err(map_field_error(FieldError::Missing { field }, line));
1539 }
1540 let year = strict_int_token::<i32>(tokens[0], fields[0], line)?;
1541 let month = strict_int_token::<i64>(tokens[1], fields[1], line)?;
1542 let day = strict_int_token::<i64>(tokens[2], fields[2], line)?;
1543 let hour = strict_int_token::<i64>(tokens[3], fields[3], line)?;
1544 let minute = strict_int_token::<i64>(tokens[4], fields[4], line)?;
1545 let second = strict_f64_token(tokens[5], fields[5], line)?;
1546 let civil = validate::civil_datetime_with_second_policy(
1547 year as i64,
1548 month,
1549 day,
1550 hour,
1551 minute,
1552 second,
1553 second_policy,
1554 )
1555 .map_err(|error| map_field_error(error, line))?;
1556 Ok(ObsEpochTime {
1557 year,
1558 month: civil.month as u8,
1559 day: civil.day as u8,
1560 hour: civil.hour as u8,
1561 minute: civil.minute as u8,
1562 second: civil.second,
1563 })
1564}
1565
1566fn strict_vec3_tokens(body: &str, line: &str, fields: [&'static str; 3]) -> Result<[f64; 3]> {
1567 let tokens: Vec<&str> = body.split_whitespace().collect();
1568 if tokens.len() < fields.len() {
1569 let field = fields[tokens.len()];
1570 return Err(map_field_error(FieldError::Missing { field }, line));
1571 }
1572 Ok([
1573 strict_f64_token(tokens[0], fields[0], line)?,
1574 strict_f64_token(tokens[1], fields[1], line)?,
1575 strict_f64_token(tokens[2], fields[2], line)?,
1576 ])
1577}
1578
1579fn strict_f64_field(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<f64> {
1580 strict_f64_token(field(line, start, end), field_name, line)
1581}
1582
1583fn optional_i64_field(
1584 line: &str,
1585 start: usize,
1586 end: usize,
1587 field_name: &'static str,
1588) -> Result<Option<i64>> {
1589 let token = field(line, start, end).trim();
1590 if token.is_empty() {
1591 Ok(None)
1592 } else {
1593 strict_int_token::<i64>(token, field_name, line).map(Some)
1594 }
1595}
1596
1597fn optional_trimmed(line: &str, start: usize, end: usize) -> Option<String> {
1598 let value = field(line, start, end).trim();
1599 (!value.is_empty()).then(|| value.to_string())
1600}
1601
1602fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
1603where
1604 T: core::str::FromStr,
1605{
1606 strict_int_token(field(line, start, end), field_name, line)
1607}
1608
1609fn strict_f64_token(token: &str, field_name: &'static str, line: &str) -> Result<f64> {
1610 validate::strict_f64(token, field_name).map_err(|error| map_field_error(error, line))
1611}
1612
1613fn validate_finite_input(value: f64, field: &'static str) -> Result<()> {
1614 if value.is_finite() {
1615 Ok(())
1616 } else {
1617 Err(Error::InvalidInput(format!(
1618 "RINEX OBS {field} must be finite"
1619 )))
1620 }
1621}
1622
1623fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
1624where
1625 T: core::str::FromStr,
1626{
1627 validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
1628}
1629
1630fn scale_factor_value(value: u32) -> Result<f64> {
1631 match value {
1632 1 | 10 | 100 | 1000 => Ok(f64::from(value)),
1633 _ => Err(Error::Parse(format!(
1634 "RINEX OBS invalid scale_factor.factor: expected 1, 10, 100, or 1000, got {value}"
1635 ))),
1636 }
1637}
1638
1639fn map_field_error(error: FieldError, line: &str) -> Error {
1640 Error::Parse(format!(
1641 "RINEX OBS invalid {}: {error} in {line:?}",
1642 error.field()
1643 ))
1644}
1645
1646fn obs_payload_field_count(payload_len: usize) -> usize {
1647 let full = payload_len / OBS_FIELD_WIDTH;
1648 let trailing = payload_len % OBS_FIELD_WIDTH;
1649 full + usize::from(trailing >= OBS_VALUE_WIDTH)
1650}
1651
1652fn sat_record_field_count(record_len: usize) -> usize {
1653 obs_payload_field_count(record_len.saturating_sub(3))
1654}
1655
1656fn ascii_fixed_columns(line: &str) -> Cow<'_, str> {
1657 if line.is_ascii() {
1658 Cow::Borrowed(line)
1659 } else {
1660 Cow::Owned(
1661 line.chars()
1662 .map(|ch| if ch.is_ascii() { ch } else { ' ' })
1663 .collect(),
1664 )
1665 }
1666}
1667
1668fn truncate_to_char_boundary(record: &mut String, len: usize) {
1669 let mut end = len.min(record.len());
1670 while !record.is_char_boundary(end) {
1671 end -= 1;
1672 }
1673 record.truncate(end);
1674}
1675
1676fn starts_with_sat_designator(line: &str) -> bool {
1684 let b = line.as_bytes();
1685 b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1].is_ascii_digit() && b[2].is_ascii_digit()
1686}
1687
1688fn consume_skipped_sat_continuations<'a, I: Iterator<Item = &'a str>>(
1692 lines: &mut std::iter::Peekable<I>,
1693) {
1694 while let Some(raw_next) = lines.peek().copied() {
1695 let next = ascii_fixed_columns(raw_next.trim_end_matches(['\r', '\n']));
1696 if next.starts_with('>') || starts_with_sat_designator(&next) {
1697 break;
1698 }
1699 lines.next();
1700 }
1701}
1702
1703fn append_sat_continuation(record: &mut String, continuation: &str, n_obs: usize) {
1704 let fields_present = sat_record_field_count(record.len());
1705 let logical_len = 3 + fields_present * OBS_FIELD_WIDTH;
1706 truncate_to_char_boundary(record, logical_len);
1707
1708 let remaining = n_obs.saturating_sub(fields_present);
1709 let payload = field(continuation, 3, continuation.len());
1710 let fields_available = obs_payload_field_count(payload.len());
1711 let fields_to_copy = remaining.min(fields_available);
1712 let width = fields_to_copy * OBS_FIELD_WIDTH;
1713 record.push_str(field(payload, 0, width));
1714}
1715
1716fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
1718 token.parse::<GnssSatelliteId>().ok()
1719}
1720
1721fn digit_at(line: &str, col: usize) -> Option<u8> {
1724 line.as_bytes()
1725 .get(col)
1726 .filter(|b| b.is_ascii_digit())
1727 .map(|b| b - b'0')
1728}
1729
1730mod write;
1731
1732#[cfg(all(test, sidereon_repo_tests))]
1733mod tests;