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;
70
71#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
76pub struct ObsEpochTime {
77 pub year: i32,
79 pub month: u8,
81 pub day: u8,
83 pub hour: u8,
85 pub minute: u8,
87 pub second: f64,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
94pub struct ObsValue {
95 pub value: Option<f64>,
98 pub lli: Option<u8>,
100 pub ssi: Option<u8>,
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub struct ObsPhaseShift {
107 pub system: GnssSystem,
109 pub code: String,
111 pub correction_cycles: f64,
113 pub satellites: Vec<GnssSatelliteId>,
116}
117
118#[derive(Debug, Clone, PartialEq)]
120pub struct ObsScaleFactor {
121 pub system: GnssSystem,
123 pub factor: f64,
125 pub codes: Vec<String>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct PgmRunByDate {
132 pub program: String,
134 pub run_by: String,
136 pub date: String,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct ReceiverInfo {
143 pub number: String,
145 pub receiver_type: String,
147 pub version: String,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct AntennaInfo {
154 pub number: String,
156 pub antenna_type: String,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub struct ObsLeapSeconds {
163 pub current: i64,
165 pub delta_future: Option<i64>,
167 pub week: Option<i64>,
169 pub day: Option<i64>,
171}
172
173#[derive(Debug, Clone, PartialEq)]
176pub struct ObsEpoch {
177 pub epoch: ObsEpochTime,
179 pub flag: u8,
181 pub rcv_clock_offset_s: Option<f64>,
183 pub epoch_picoseconds: Option<u32>,
185 pub declared_record_count: usize,
187 pub special_record_count: usize,
189 pub sats: BTreeMap<GnssSatelliteId, Vec<ObsValue>>,
192}
193
194#[derive(Debug, Clone, PartialEq)]
196pub struct ObsHeader {
197 pub version: f64,
199 pub approx_position_m: Option<[f64; 3]>,
202 pub antenna_delta_hen_m: Option<[f64; 3]>,
206 pub obs_codes: BTreeMap<GnssSystem, Vec<String>>,
208 pub program_run_by_date: Option<PgmRunByDate>,
210 pub comments: Vec<String>,
212 pub marker_number: Option<String>,
214 pub marker_type: Option<String>,
216 pub observer: Option<String>,
218 pub agency: Option<String>,
220 pub receiver: Option<ReceiverInfo>,
222 pub antenna: Option<AntennaInfo>,
224 pub interval_s: Option<f64>,
226 pub time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
228 pub time_of_last_obs: Option<(ObsEpochTime, TimeScale)>,
230 pub n_satellites: Option<usize>,
232 pub prn_obs_counts: BTreeMap<GnssSatelliteId, Vec<Option<usize>>>,
234 pub phase_shifts: Vec<ObsPhaseShift>,
236 pub scale_factors: Vec<ObsScaleFactor>,
238 pub glonass_slots: BTreeMap<u8, i8>,
240 pub glonass_cod_phs_bis: Option<Vec<(String, f64)>>,
242 pub signal_strength_unit: Option<String>,
244 pub leap_seconds: Option<ObsLeapSeconds>,
246 pub marker_name: Option<String>,
248 pub unretained_header_labels: Vec<String>,
250}
251
252#[derive(Debug, Clone, PartialEq)]
258pub struct RinexObs {
259 pub header: ObsHeader,
261 pub epochs: Vec<ObsEpoch>,
264 pub skipped_records: usize,
271}
272
273impl RinexObs {
274 pub fn parse(text: &str) -> Result<Self> {
280 let mut parser = Parser::new();
281 let mut lines = text.lines();
282 parser.parse_header(&mut lines)?;
283 let mut body = lines.peekable();
284 if parser.is_rinex2() {
285 parser.parse_body_v2(&mut body)?;
286 } else {
287 parser.parse_body(&mut body)?;
288 }
289 parser.finish()
290 }
291
292 pub fn header(&self) -> &ObsHeader {
294 &self.header
295 }
296
297 pub fn epochs(&self) -> &[ObsEpoch] {
299 &self.epochs
300 }
301
302 pub fn obs_codes(&self, sys: GnssSystem) -> Option<&[String]> {
304 self.header.obs_codes.get(&sys).map(Vec::as_slice)
305 }
306}
307
308impl core::str::FromStr for RinexObs {
309 type Err = Error;
310
311 fn from_str(s: &str) -> Result<Self> {
312 Self::parse(s)
313 }
314}
315
316#[derive(Debug, Clone, PartialEq)]
323pub struct SignalPolicy {
324 pub codes: BTreeMap<GnssSystem, Vec<String>>,
326}
327
328impl SignalPolicy {
329 pub fn default_for(version: f64) -> Result<Self> {
339 validate_finite_input(version, "version")?;
340 let mut codes = BTreeMap::new();
341 codes.insert(GnssSystem::Gps, vec!["C1C".to_string()]);
342 codes.insert(
343 GnssSystem::Galileo,
344 vec!["C1C".to_string(), "C1X".to_string()],
345 );
346 let beidou = if (3.015..3.025).contains(&version) {
351 vec!["C1I".to_string(), "C2I".to_string()]
352 } else {
353 vec!["C2I".to_string(), "C1I".to_string()]
354 };
355 codes.insert(GnssSystem::BeiDou, beidou);
356 codes.insert(GnssSystem::Glonass, vec!["C1C".to_string()]);
357 Ok(Self { codes })
358 }
359
360 pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
362 self.codes.insert(sys, codes);
363 self
364 }
365}
366
367#[derive(Debug, Clone, Default, PartialEq, Eq)]
373pub struct ObservationFilter {
374 pub codes: BTreeMap<GnssSystem, Vec<String>>,
376}
377
378impl ObservationFilter {
379 pub fn all() -> Self {
381 Self::default()
382 }
383
384 pub fn from_entries<I>(entries: I) -> Self
386 where
387 I: IntoIterator<Item = (GnssSystem, Vec<String>)>,
388 {
389 Self {
390 codes: entries.into_iter().collect(),
391 }
392 }
393
394 fn allowed_codes(&self, system: GnssSystem) -> Option<&[String]> {
395 if self.codes.is_empty() {
396 Some(&[])
397 } else {
398 self.codes.get(&system).map(Vec::as_slice)
399 }
400 }
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405pub enum ObservationKind {
406 Pseudorange,
408 CarrierPhase,
410 Doppler,
412 SignalStrength,
414 Unknown,
416}
417
418impl ObservationKind {
419 pub fn from_code(code: &str) -> Self {
421 match code.as_bytes().first().copied() {
422 Some(b'C') => Self::Pseudorange,
423 Some(b'L') => Self::CarrierPhase,
424 Some(b'D') => Self::Doppler,
425 Some(b'S') => Self::SignalStrength,
426 _ => Self::Unknown,
427 }
428 }
429
430 pub fn as_str(self) -> &'static str {
432 match self {
433 Self::Pseudorange => "pseudorange",
434 Self::CarrierPhase => "carrier_phase",
435 Self::Doppler => "doppler",
436 Self::SignalStrength => "signal_strength",
437 Self::Unknown => "unknown",
438 }
439 }
440
441 pub fn units_str(self) -> &'static str {
443 match self {
444 Self::Pseudorange => "meters",
445 Self::CarrierPhase => "cycles",
446 Self::Doppler => "hz",
447 Self::SignalStrength => "db_hz",
448 Self::Unknown => "unknown",
449 }
450 }
451}
452
453#[derive(Debug, Clone, PartialEq)]
455pub struct ObservationValueRow {
456 pub code: String,
458 pub kind: ObservationKind,
460 pub value: Option<f64>,
462 pub lli: Option<u8>,
464 pub ssi: Option<u8>,
466}
467
468#[derive(Debug, Clone, PartialEq)]
470pub struct CarrierPhaseRow {
471 pub code: String,
473 pub value_cycles: Option<f64>,
475 pub lli: Option<u8>,
477 pub ssi: Option<u8>,
479 pub frequency_hz: Option<f64>,
481 pub wavelength_m: Option<f64>,
483 pub value_m: Option<f64>,
485 pub phase_shift_cycles: f64,
489}
490
491pub fn observation_values(
493 obs: &RinexObs,
494 epoch: &ObsEpoch,
495 filter: &ObservationFilter,
496) -> Result<Vec<(GnssSatelliteId, Vec<ObservationValueRow>)>> {
497 let mut out = Vec::new();
498 for (sat, values) in epoch
499 .sats
500 .iter()
501 .filter(|(sat, _)| filter.allowed_codes(sat.system).is_some())
502 {
503 let allowed_codes = filter
504 .allowed_codes(sat.system)
505 .expect("filter presence checked");
506 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
507 continue;
508 };
509 let mut rows = Vec::new();
510 for (code, value) in code_list.iter().zip(values.iter()) {
511 if !allowed_codes.is_empty() && !allowed_codes.iter().any(|c| c == code) {
512 continue;
513 }
514 if let Some(value) = value.value {
515 validate_finite_input(value, "observation.value")?;
516 }
517 let kind = ObservationKind::from_code(code);
518 rows.push(ObservationValueRow {
519 code: code.clone(),
520 kind,
521 value: value.value,
522 lli: value.lli,
523 ssi: value.ssi,
524 });
525 }
526 out.push((*sat, rows));
527 }
528 Ok(out)
529}
530
531pub fn carrier_phase_rows(
533 obs: &RinexObs,
534 epoch: &ObsEpoch,
535 filter: &ObservationFilter,
536) -> Result<Vec<(GnssSatelliteId, Vec<CarrierPhaseRow>)>> {
537 validate_finite_input(obs.header.version, "version")?;
538 let mut out = Vec::new();
539 for (sat, rows) in observation_values(obs, epoch, filter)? {
540 let phases = rows
541 .into_iter()
542 .filter(|row| row.kind == ObservationKind::CarrierPhase)
543 .map(|row| carrier_phase_row(obs, sat, row))
544 .collect::<Result<Vec<_>>>()?;
545 out.push((sat, phases));
546 }
547 Ok(out)
548}
549
550pub fn band_frequency_hz(
555 system: GnssSystem,
556 band: char,
557 glonass_channel: Option<i8>,
558) -> Option<f64> {
559 rinex_band_frequency_hz(system, band, glonass_channel)
560}
561
562pub fn observation_frequency_hz(
564 system: GnssSystem,
565 code: &str,
566 rinex_version: f64,
567 glonass_channel: Option<i8>,
568) -> Result<Option<f64>> {
569 validate_finite_input(rinex_version, "version")?;
570 Ok(rinex_observation_frequency_hz(
571 system,
572 code,
573 rinex_version,
574 glonass_channel,
575 ))
576}
577
578fn carrier_phase_row(
579 obs: &RinexObs,
580 sat: GnssSatelliteId,
581 row: ObservationValueRow,
582) -> Result<CarrierPhaseRow> {
583 let glonass_channel = obs.header.glonass_slots.get(&sat.prn).copied();
584 let frequency_hz =
585 observation_frequency_hz(sat.system, &row.code, obs.header.version, glonass_channel)?;
586 let phase_shift_cycles = phase_shift_cycles(obs, sat, &row.code);
587 let value_cycles = row.value;
588 let wavelength_m =
589 rinex_observation_wavelength_m(sat.system, &row.code, obs.header.version, glonass_channel);
590 let value_m = match value_cycles.zip(wavelength_m) {
591 Some((cycles, lambda)) => {
592 let value_m = cycles * lambda;
593 validate_finite_input(value_m, "carrier_phase.value_m")?;
594 Some(value_m)
595 }
596 None => None,
597 };
598 Ok(CarrierPhaseRow {
599 code: row.code,
600 value_cycles,
601 lli: row.lli,
602 ssi: row.ssi,
603 frequency_hz,
604 wavelength_m,
605 value_m,
606 phase_shift_cycles,
607 })
608}
609
610fn phase_shift_cycles(obs: &RinexObs, sat: GnssSatelliteId, code: &str) -> f64 {
611 let mut system_wide = None;
612 for shift in obs.header.phase_shifts.iter().rev() {
613 if shift.system != sat.system || shift.code != code {
614 continue;
615 }
616 if shift.satellites.is_empty() {
617 if system_wide.is_none() {
618 system_wide = Some(shift.correction_cycles);
619 }
620 } else if shift.satellites.contains(&sat) {
621 return shift.correction_cycles;
622 }
623 }
624 system_wide.unwrap_or(0.0)
625}
626
627pub fn pseudoranges(
634 obs: &RinexObs,
635 epoch: &ObsEpoch,
636 policy: &SignalPolicy,
637) -> Result<Vec<(GnssSatelliteId, f64)>> {
638 let mut out = Vec::new();
639 for (sat, values) in &epoch.sats {
640 let Some(prefs) = policy.codes.get(&sat.system) else {
641 continue;
642 };
643 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
644 continue;
645 };
646 for code in prefs {
647 if let Some(idx) = code_list.iter().position(|c| c == code) {
648 if let Some(ObsValue {
649 value: Some(range_m),
650 ..
651 }) = values.get(idx)
652 {
653 validate_finite_input(*range_m, "pseudorange_m")?;
654 out.push((*sat, *range_m));
655 break;
656 }
657 }
658 }
659 }
660 Ok(out)
661}
662
663struct Parser {
665 version: Option<f64>,
666 is_observation: bool,
667 approx_position_m: Option<[f64; 3]>,
668 antenna_delta_hen_m: Option<[f64; 3]>,
669 obs_codes: BTreeMap<GnssSystem, Vec<String>>,
670 interval_s: Option<f64>,
671 time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
672 time_of_last_obs: Option<(ObsEpochTime, TimeScale)>,
673 program_run_by_date: Option<PgmRunByDate>,
674 comments: Vec<String>,
675 marker_number: Option<String>,
676 marker_type: Option<String>,
677 observer: Option<String>,
678 agency: Option<String>,
679 receiver: Option<ReceiverInfo>,
680 antenna: Option<AntennaInfo>,
681 n_satellites: Option<usize>,
682 prn_obs_counts: BTreeMap<GnssSatelliteId, Vec<Option<usize>>>,
683 phase_shifts: Vec<ObsPhaseShift>,
684 scale_factors: Vec<ObsScaleFactor>,
685 scale_factor_continuation: Option<ScaleFactorContinuation>,
686 glonass_slots: BTreeMap<u8, i8>,
687 glonass_slots_remaining: Option<usize>,
688 glonass_cod_phs_bis: Option<Vec<(String, f64)>>,
689 signal_strength_unit: Option<String>,
690 leap_seconds: Option<ObsLeapSeconds>,
691 marker_name: Option<String>,
692 unretained_header_labels: Vec<String>,
693 epochs: Vec<ObsEpoch>,
694 current_obs_sys: Option<GnssSystem>,
697 obs_codes_remaining: usize,
699 rinex2_default_system: Option<GnssSystem>,
701 rinex2_obs_codes: Vec<String>,
704 rinex2_obs_codes_remaining: usize,
706 diagnostics: Diagnostics,
711}
712
713#[derive(Debug, Clone, Copy)]
714struct ScaleFactorContinuation {
715 remaining: usize,
716}
717
718impl Parser {
719 fn new() -> Self {
720 Self {
721 version: None,
722 is_observation: false,
723 approx_position_m: None,
724 antenna_delta_hen_m: None,
725 obs_codes: BTreeMap::new(),
726 interval_s: None,
727 time_of_first_obs: None,
728 time_of_last_obs: None,
729 program_run_by_date: None,
730 comments: Vec::new(),
731 marker_number: None,
732 marker_type: None,
733 observer: None,
734 agency: None,
735 receiver: None,
736 antenna: None,
737 n_satellites: None,
738 prn_obs_counts: BTreeMap::new(),
739 phase_shifts: Vec::new(),
740 scale_factors: Vec::new(),
741 scale_factor_continuation: None,
742 glonass_slots: BTreeMap::new(),
743 glonass_slots_remaining: None,
744 glonass_cod_phs_bis: None,
745 signal_strength_unit: None,
746 leap_seconds: None,
747 marker_name: None,
748 unretained_header_labels: Vec::new(),
749 epochs: Vec::new(),
750 current_obs_sys: None,
751 obs_codes_remaining: 0,
752 rinex2_default_system: None,
753 rinex2_obs_codes: Vec::new(),
754 rinex2_obs_codes_remaining: 0,
755 diagnostics: Diagnostics::new(),
756 }
757 }
758
759 fn is_rinex2(&self) -> bool {
760 self.version
761 .is_some_and(|version| version.floor() as i64 == 2)
762 }
763
764 fn push_unrepresentable_satellite_skip(&mut self, token: &str) {
767 self.diagnostics.push_skip(Skip {
768 at: RecordRef::default().with_satellite(token.trim()),
769 reason: SkipReason::UnrepresentableSatellite,
770 });
771 }
772
773 fn parse_header<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut I) -> Result<()> {
774 let mut saw_end = false;
775 for raw in lines.by_ref() {
776 let line = raw.trim_end_matches(['\r', '\n']);
777 let label = raw_field_from(line, 60).trim();
778 match label {
779 "RINEX VERSION / TYPE" => self.parse_version(line)?,
780 "PGM / RUN BY / DATE" => self.parse_pgm_run_by_date(line),
781 "COMMENT" => self.comments.push(field(line, 0, 60).trim().to_string()),
782 "APPROX POSITION XYZ" => self.parse_approx_position(line)?,
783 "ANTENNA: DELTA H/E/N" => self.parse_antenna_delta(line)?,
784 "SYS / # / OBS TYPES" => self.parse_obs_types(line)?,
785 "# / TYPES OF OBSERV" => self.parse_obs_types_v2(line)?,
786 "SYS / SCALE FACTOR" => self.parse_scale_factor(line)?,
787 "SYS / PHASE SHIFT" => self.parse_phase_shift(line)?,
788 "TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
789 "TIME OF LAST OBS" => self.parse_time_of_last_obs(line)?,
790 "INTERVAL" => {
791 self.interval_s = Some(strict_f64_field(line, 0, 10, "interval_s")?);
792 }
793 "GLONASS SLOT / FRQ #" => self.parse_glonass_slots(line)?,
794 "GLONASS COD/PHS/BIS" => self.parse_glonass_cod_phs_bis(line)?,
795 "SIGNAL STRENGTH UNIT" => {
796 let unit = field(line, 0, 20).trim();
797 if !unit.is_empty() {
798 self.signal_strength_unit = Some(unit.to_string());
799 }
800 }
801 "LEAP SECONDS" => self.parse_leap_seconds(line)?,
802 "# OF SATELLITES" => {
803 self.n_satellites =
804 Some(strict_int_field::<usize>(line, 0, 6, "n_satellites")?);
805 }
806 "PRN / # OF OBS" => self.parse_prn_obs_counts(line)?,
807 "MARKER NAME" => {
808 let name = field(line, 0, 60).trim();
809 if !name.is_empty() {
810 self.marker_name = Some(name.to_string());
811 }
812 }
813 "MARKER NUMBER" => {
814 self.marker_number = optional_trimmed(line, 0, 20);
815 }
816 "MARKER TYPE" => {
817 self.marker_type = optional_trimmed(line, 0, 20);
818 }
819 "OBSERVER / AGENCY" => {
820 self.observer = optional_trimmed(line, 0, 20);
821 self.agency = optional_trimmed(line, 20, 60);
822 }
823 "REC # / TYPE / VERS" => {
824 self.receiver = Some(ReceiverInfo {
825 number: field(line, 0, 20).trim().to_string(),
826 receiver_type: field(line, 20, 40).trim().to_string(),
827 version: field(line, 40, 60).trim().to_string(),
828 });
829 }
830 "ANT # / TYPE" => {
831 self.antenna = Some(AntennaInfo {
832 number: field(line, 0, 20).trim().to_string(),
833 antenna_type: field(line, 20, 40).trim().to_string(),
834 });
835 }
836 "END OF HEADER" => {
837 self.ensure_obs_type_count_complete(line)?;
838 self.ensure_obs_type_count_complete_v2(line)?;
839 self.ensure_scale_factor_count_complete(line)?;
840 saw_end = true;
841 break;
842 }
843 _ => {
846 if !label.is_empty() {
847 self.unretained_header_labels.push(label.to_string());
848 }
849 }
850 }
851 }
852 if !saw_end {
853 return Err(Error::Parse("RINEX OBS header has no END OF HEADER".into()));
854 }
855 Ok(())
856 }
857
858 fn parse_version(&mut self, line: &str) -> Result<()> {
859 let version = field(line, 0, 20).trim();
860 let version = strict_f64_token(version, "version", line)?;
861 let type_field = field(line, 20, 40);
863 self.is_observation =
864 type_field.trim_start().starts_with('O') || type_field.contains("OBSERVATION");
865 if !self.is_observation {
866 return Err(Error::Parse(format!(
867 "RINEX file is not observation data: {type_field:?}"
868 )));
869 }
870 if !matches!(version.floor() as i64, 2..=4) {
871 return Err(Error::Parse(format!(
872 "RINEX OBS parser requires major version 2, 3, or 4, got {version}"
873 )));
874 }
875 if version.floor() as i64 == 2 {
876 let system_field = field(line, 40, 41).trim();
877 if let Some(letter) = system_field.chars().next().filter(|letter| *letter != 'M') {
878 self.rinex2_default_system = GnssSystem::from_letter(letter);
879 }
880 }
881 self.version = Some(version);
882 Ok(())
883 }
884
885 fn parse_approx_position(&mut self, line: &str) -> Result<()> {
886 let body = field(line, 0, 60);
887 self.approx_position_m = Some(strict_vec3_tokens(
888 body,
889 line,
890 [
891 "approx_position.x_m",
892 "approx_position.y_m",
893 "approx_position.z_m",
894 ],
895 )?);
896 Ok(())
897 }
898
899 fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
900 let body = field(line, 0, 60);
901 self.antenna_delta_hen_m = Some(strict_vec3_tokens(
902 body,
903 line,
904 [
905 "antenna_delta.height_m",
906 "antenna_delta.east_m",
907 "antenna_delta.north_m",
908 ],
909 )?);
910 Ok(())
911 }
912
913 fn parse_pgm_run_by_date(&mut self, line: &str) {
914 self.program_run_by_date = Some(PgmRunByDate {
915 program: field(line, 0, 20).trim().to_string(),
916 run_by: field(line, 20, 40).trim().to_string(),
917 date: field(line, 40, 60).trim().to_string(),
918 });
919 }
920
921 fn parse_obs_types(&mut self, line: &str) -> Result<()> {
922 let sys_field = field(line, 0, 1).trim();
926 if !sys_field.is_empty() {
927 self.ensure_obs_type_count_complete(line)?;
928 let letter = sys_field.chars().next().unwrap();
929 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
930 Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
931 })?;
932 let count = strict_int_field::<usize>(line, 3, 6, "obs_type_count")?;
933 self.current_obs_sys = Some(system);
934 self.obs_codes_remaining = count;
935 self.obs_codes.entry(system).or_default();
936 }
937 let Some(system) = self.current_obs_sys else {
938 return Ok(());
939 };
940 let codes_section = field(line, 7, 60);
943 let list = self.obs_codes.get_mut(&system).expect("system inserted");
944 for tok in codes_section.split_whitespace() {
945 if self.obs_codes_remaining == 0 {
946 return Err(Error::Parse(format!(
947 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
948 )));
949 }
950 list.push(tok.to_string());
951 self.obs_codes_remaining -= 1;
952 }
953 Ok(())
954 }
955
956 fn parse_obs_types_v2(&mut self, line: &str) -> Result<()> {
957 if field(line, 0, 6).trim().is_empty() {
958 if self.rinex2_obs_codes_remaining == 0 {
959 return Ok(());
960 }
961 } else {
962 self.ensure_obs_type_count_complete_v2(line)?;
963 self.rinex2_obs_codes.clear();
964 self.rinex2_obs_codes_remaining =
965 strict_int_field::<usize>(line, 0, 6, "rinex2.obs_type_count")?;
966 }
967 for code in field(line, 6, 60).split_whitespace() {
968 if self.rinex2_obs_codes_remaining == 0 {
969 return Err(Error::Parse(format!(
970 "RINEX OBS # / TYPES OF OBSERV lists more codes than declared in {line:?}"
971 )));
972 }
973 self.rinex2_obs_codes.push(code.to_string());
974 self.rinex2_obs_codes_remaining -= 1;
975 }
976 Ok(())
977 }
978
979 fn ensure_obs_type_count_complete(&self, line: &str) -> Result<()> {
980 if self.obs_codes_remaining == 0 {
981 return Ok(());
982 }
983 let Some(system) = self.current_obs_sys else {
984 return Ok(());
985 };
986 let supplied = self.obs_codes.get(&system).map_or(0, Vec::len);
987 let declared = supplied + self.obs_codes_remaining;
988 Err(Error::Parse(format!(
989 "RINEX OBS {system} SYS / # / OBS TYPES declares {declared} codes but supplies {supplied} before {line:?}"
990 )))
991 }
992
993 fn ensure_obs_type_count_complete_v2(&self, line: &str) -> Result<()> {
994 if self.rinex2_obs_codes_remaining == 0 {
995 return Ok(());
996 }
997 let supplied = self.rinex2_obs_codes.len();
998 let declared = supplied + self.rinex2_obs_codes_remaining;
999 Err(Error::Parse(format!(
1000 "RINEX OBS # / TYPES OF OBSERV declares {declared} codes but supplies {supplied} before {line:?}"
1001 )))
1002 }
1003
1004 fn parse_phase_shift(&mut self, line: &str) -> Result<()> {
1005 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
1006 if tokens.is_empty() {
1007 return Ok(());
1008 }
1009 if tokens.len() < 2 {
1010 return Err(Error::Parse(format!(
1011 "RINEX OBS phase-shift header has too few fields in {line:?}"
1012 )));
1013 }
1014
1015 let system = tokens[0]
1016 .chars()
1017 .next()
1018 .and_then(GnssSystem::from_letter)
1019 .ok_or_else(|| {
1020 Error::Parse(format!(
1021 "RINEX OBS phase-shift system unparsable in {line:?}"
1022 ))
1023 })?;
1024 let code = tokens[1].to_string();
1025 let correction_cycles = match tokens.get(2) {
1026 Some(token) => strict_f64_token(token, "phase_shift.correction_cycles", line)?,
1027 None => 0.0,
1028 };
1029
1030 let satellites = if let Some(count_token) = tokens.get(3) {
1031 let count =
1032 strict_int_token::<usize>(count_token, "phase_shift.satellite_count", line)?;
1033 let sat_tokens = &tokens[4..];
1034 if sat_tokens.len() != count {
1035 return Err(Error::Parse(format!(
1036 "RINEX OBS phase-shift satellite count mismatch in {line:?}"
1037 )));
1038 }
1039 sat_tokens
1040 .iter()
1041 .map(|token| {
1042 parse_sv_token(token).ok_or_else(|| {
1043 Error::Parse(format!(
1044 "RINEX OBS phase-shift satellite token {token:?} unparsable in {line:?}"
1045 ))
1046 })
1047 })
1048 .collect::<Result<Vec<_>>>()?
1049 } else {
1050 Vec::new()
1051 };
1052
1053 self.phase_shifts.push(ObsPhaseShift {
1054 system,
1055 code,
1056 correction_cycles,
1057 satellites,
1058 });
1059 Ok(())
1060 }
1061
1062 fn parse_scale_factor(&mut self, line: &str) -> Result<()> {
1063 let sys_field = field(line, 0, 1).trim();
1064 if !sys_field.is_empty() {
1065 self.ensure_scale_factor_count_complete(line)?;
1066 let letter = sys_field.chars().next().unwrap();
1067 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
1068 Error::Parse(format!("RINEX OBS unknown scale-factor system {letter:?}"))
1069 })?;
1070 let factor =
1071 scale_factor_value(strict_int_field::<u32>(line, 2, 6, "scale_factor.factor")?)?;
1072 let count_field = field(line, 8, 10).trim();
1073 let count = if count_field.is_empty() {
1074 0
1075 } else {
1076 strict_int_token::<usize>(count_field, "scale_factor.obs_type_count", line)?
1077 };
1078 self.scale_factors.push(ObsScaleFactor {
1079 system,
1080 factor,
1081 codes: Vec::new(),
1082 });
1083 if count == 0 {
1084 return Ok(());
1085 }
1086 self.scale_factor_continuation = Some(ScaleFactorContinuation { remaining: count });
1087 }
1088
1089 self.collect_scale_factor_codes(line)
1090 }
1091
1092 fn collect_scale_factor_codes(&mut self, line: &str) -> Result<()> {
1093 let Some(mut continuation) = self.scale_factor_continuation else {
1094 return Ok(());
1095 };
1096 let record = self
1097 .scale_factors
1098 .last_mut()
1099 .expect("scale factor continuation has a record");
1100 for code in field(line, 10, 60).split_whitespace() {
1101 if continuation.remaining == 0 {
1102 return Err(Error::Parse(format!(
1103 "RINEX OBS SYS / SCALE FACTOR lists more codes than declared in {line:?}"
1104 )));
1105 }
1106 record.codes.push(code.to_string());
1107 continuation.remaining -= 1;
1108 }
1109 self.scale_factor_continuation = (continuation.remaining > 0).then_some(continuation);
1110 Ok(())
1111 }
1112
1113 fn ensure_scale_factor_count_complete(&self, line: &str) -> Result<()> {
1114 let Some(continuation) = self.scale_factor_continuation else {
1115 return Ok(());
1116 };
1117 let supplied = self
1118 .scale_factors
1119 .last()
1120 .map_or(0, |record| record.codes.len());
1121 let declared = supplied + continuation.remaining;
1122 Err(Error::Parse(format!(
1123 "RINEX OBS SYS / SCALE FACTOR declares {declared} codes but supplies {supplied} before {line:?}"
1124 )))
1125 }
1126
1127 fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
1128 self.time_of_first_obs = Some(self.parse_time_header(line, "time_of_first_obs")?);
1129 Ok(())
1130 }
1131
1132 fn parse_time_of_last_obs(&mut self, line: &str) -> Result<()> {
1133 self.time_of_last_obs = Some(self.parse_time_header(line, "time_of_last_obs")?);
1134 Ok(())
1135 }
1136
1137 fn parse_time_header(
1138 &self,
1139 line: &str,
1140 prefix: &'static str,
1141 ) -> Result<(ObsEpochTime, TimeScale)> {
1142 let body = field(line, 0, 43);
1143 let scale_label = field(line, 48, 51).trim();
1144 let scale = time_scale_from_label(scale_label, line)?;
1145 let year = match prefix {
1146 "time_of_last_obs" => "time_of_last_obs.year",
1147 _ => "time_of_first_obs.year",
1148 };
1149 let month = match prefix {
1150 "time_of_last_obs" => "time_of_last_obs.month",
1151 _ => "time_of_first_obs.month",
1152 };
1153 let day = match prefix {
1154 "time_of_last_obs" => "time_of_last_obs.day",
1155 _ => "time_of_first_obs.day",
1156 };
1157 let hour = match prefix {
1158 "time_of_last_obs" => "time_of_last_obs.hour",
1159 _ => "time_of_first_obs.hour",
1160 };
1161 let minute = match prefix {
1162 "time_of_last_obs" => "time_of_last_obs.minute",
1163 _ => "time_of_first_obs.minute",
1164 };
1165 let second = match prefix {
1166 "time_of_last_obs" => "time_of_last_obs.second",
1167 _ => "time_of_first_obs.second",
1168 };
1169 let epoch = parse_epoch_time_tokens(
1170 body,
1171 line,
1172 [year, month, day, hour, minute, second],
1173 civil_second_policy_for_time_scale(scale),
1174 )?;
1175 Ok((epoch, scale))
1176 }
1177
1178 fn parse_glonass_slots(&mut self, line: &str) -> Result<()> {
1179 let count_field = field(line, 0, 3).trim();
1181 if !count_field.is_empty() {
1182 let count = strict_int_token::<usize>(count_field, "glonass_slot.count", line)?;
1183 self.glonass_slots_remaining = Some(count);
1184 }
1185 let body = field(line, 4, 60);
1186 let tokens: Vec<&str> = body.split_whitespace().collect();
1187 if !tokens.len().is_multiple_of(2) {
1188 return Err(Error::Parse(format!(
1189 "RINEX OBS GLONASS slot table has an odd token count in {line:?}"
1190 )));
1191 }
1192 for pair in tokens.chunks_exact(2) {
1193 if let Some(remaining) = self.glonass_slots_remaining.as_mut() {
1197 if *remaining == 0 {
1198 return Err(Error::Parse(format!(
1199 "RINEX OBS GLONASS slot table has more entries than declared in {line:?}"
1200 )));
1201 }
1202 *remaining -= 1;
1203 }
1204 let Some(sat) = parse_sv_token(pair[0]) else {
1210 self.push_unrepresentable_satellite_skip(pair[0]);
1211 continue;
1212 };
1213 if sat.system != GnssSystem::Glonass {
1214 return Err(Error::Parse(format!(
1215 "RINEX OBS GLONASS slot token {:?} is not GLONASS in {line:?}",
1216 pair[0]
1217 )));
1218 }
1219 let channel = strict_int_token::<i8>(pair[1], "glonass_slot.channel", line)?;
1220 if !valid_glonass_frequency_channel(i32::from(channel)) {
1221 return Err(Error::Parse(format!(
1222 "RINEX OBS invalid glonass_slot.channel: {channel} out of range in {line:?}"
1223 )));
1224 }
1225 self.glonass_slots.insert(sat.prn, channel);
1226 }
1227 Ok(())
1228 }
1229
1230 fn parse_glonass_cod_phs_bis(&mut self, line: &str) -> Result<()> {
1231 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
1232 let mut entries = Vec::new();
1233 for pair in tokens.chunks(2) {
1234 if pair.len() != 2 {
1235 return Err(Error::Parse(format!(
1236 "RINEX OBS GLONASS COD/PHS/BIS has an odd token count in {line:?}"
1237 )));
1238 }
1239 entries.push((
1240 pair[0].to_string(),
1241 strict_f64_token(pair[1], "glonass_code_phase_bias", line)?,
1242 ));
1243 }
1244 self.glonass_cod_phs_bis = Some(entries);
1245 Ok(())
1246 }
1247
1248 fn parse_leap_seconds(&mut self, line: &str) -> Result<()> {
1249 let current = strict_int_field::<i64>(line, 0, 6, "leap_seconds.current")?;
1250 self.leap_seconds = Some(ObsLeapSeconds {
1251 current,
1252 delta_future: optional_i64_field(line, 6, 12, "leap_seconds.delta_future")?,
1253 week: optional_i64_field(line, 12, 18, "leap_seconds.week")?,
1254 day: optional_i64_field(line, 18, 24, "leap_seconds.day")?,
1255 });
1256 Ok(())
1257 }
1258
1259 fn parse_prn_obs_counts(&mut self, line: &str) -> Result<()> {
1260 let token = field(line, 0, 3).trim();
1261 if token.is_empty() {
1262 return Ok(());
1263 }
1264 let Some(sat) = parse_sv_token(token) else {
1265 self.push_unrepresentable_satellite_skip(token);
1266 return Ok(());
1267 };
1268 let count = self.obs_codes.get(&sat.system).map_or(0, Vec::len);
1269 let mut values = Vec::with_capacity(count);
1270 for idx in 0..count {
1271 let start = 3 + idx * 6;
1272 let raw = field(line, start, start + 6).trim();
1273 if raw.is_empty() {
1274 values.push(None);
1275 } else {
1276 values.push(Some(strict_int_token::<usize>(raw, "prn_obs_count", line)?));
1277 }
1278 }
1279 self.prn_obs_counts.insert(sat, values);
1280 Ok(())
1281 }
1282
1283 fn parse_body<'a, I: Iterator<Item = &'a str>>(
1284 &mut self,
1285 lines: &mut std::iter::Peekable<I>,
1286 ) -> Result<()> {
1287 while let Some(raw) = lines.next() {
1288 let line = raw.trim_end_matches(['\r', '\n']);
1289 if line.is_empty() {
1290 continue;
1291 }
1292 if !line.starts_with('>') {
1293 continue;
1295 }
1296 let time_scale = self
1297 .time_of_first_obs
1298 .map_or(TimeScale::Gpst, |(_, scale)| scale);
1299 let (epoch_time, flag, numsat, rcv_clock_offset_s, epoch_picoseconds) =
1300 parse_epoch_line(line, civil_second_policy_for_time_scale(time_scale))?;
1301
1302 if flag > 1 {
1303 for _ in 0..numsat {
1307 lines
1308 .next()
1309 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
1310 }
1311 self.epochs.push(ObsEpoch {
1312 epoch: epoch_time,
1313 flag,
1314 rcv_clock_offset_s,
1315 epoch_picoseconds,
1316 declared_record_count: numsat,
1317 special_record_count: numsat,
1318 sats: BTreeMap::new(),
1319 });
1320 continue;
1321 }
1322
1323 let mut sats = BTreeMap::new();
1324 for _ in 0..numsat {
1325 let sat_line = lines.next().ok_or_else(|| {
1326 Error::Parse("RINEX OBS epoch truncated: missing satellite line".into())
1327 })?;
1328 let sat_line = sat_line.trim_end_matches(['\r', '\n']);
1329 let normalized = ascii_fixed_columns(sat_line);
1336 if !starts_with_sat_designator(&normalized) {
1337 return Err(Error::Parse(
1342 "RINEX OBS epoch truncated: expected satellite record".into(),
1343 ));
1344 }
1345 if parse_sv_token(field(&normalized, 0, 3)).is_none() {
1346 self.push_unrepresentable_satellite_skip(field(&normalized, 0, 3));
1351 consume_skipped_sat_continuations(lines);
1352 continue;
1353 }
1354 let sat_record = self.collect_sat_record(sat_line, lines)?;
1355 let (sat, values) = self.parse_sat_line(&sat_record)?;
1356 sats.insert(sat, values);
1357 }
1358 self.epochs.push(ObsEpoch {
1359 epoch: epoch_time,
1360 flag,
1361 rcv_clock_offset_s,
1362 epoch_picoseconds,
1363 declared_record_count: numsat,
1364 special_record_count: 0,
1365 sats,
1366 });
1367 }
1368 Ok(())
1369 }
1370
1371 fn parse_body_v2<'a, I: Iterator<Item = &'a str>>(
1372 &mut self,
1373 lines: &mut std::iter::Peekable<I>,
1374 ) -> Result<()> {
1375 while let Some(raw) = lines.next() {
1376 let line = raw.trim_end_matches(['\r', '\n']);
1377 if line.is_empty() {
1378 continue;
1379 }
1380 let time_scale = self
1381 .time_of_first_obs
1382 .map_or(TimeScale::Gpst, |(_, scale)| scale);
1383 let (epoch_time, flag, numsat, rcv_clock_offset_s) =
1384 parse_epoch_line_v2(line, civil_second_policy_for_time_scale(time_scale))?;
1385
1386 if flag > 1 {
1387 for _ in 0..numsat {
1388 lines
1389 .next()
1390 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
1391 }
1392 self.epochs.push(ObsEpoch {
1393 epoch: epoch_time,
1394 flag,
1395 rcv_clock_offset_s,
1396 epoch_picoseconds: None,
1397 declared_record_count: numsat,
1398 special_record_count: numsat,
1399 sats: BTreeMap::new(),
1400 });
1401 continue;
1402 }
1403
1404 let sv_tokens = collect_epoch_sv_tokens_v2(line, numsat, lines)?;
1405 let obs_lines_per_sat = self.rinex2_obs_lines_per_sat()?;
1406 let mut sats = BTreeMap::new();
1407 for token in sv_tokens {
1408 let mut obs_lines = Vec::with_capacity(obs_lines_per_sat);
1409 for _ in 0..obs_lines_per_sat {
1410 let obs_line = lines.next().ok_or_else(|| {
1411 Error::Parse("RINEX OBS epoch truncated: missing observation line".into())
1412 })?;
1413 obs_lines.push(obs_line.trim_end_matches(['\r', '\n']).to_string());
1414 }
1415
1416 let Some(sat) = self.parse_sv_token_v2(&token) else {
1417 self.push_unrepresentable_satellite_skip(&token);
1418 continue;
1419 };
1420 self.ensure_rinex2_system_obs_codes(sat.system);
1421 let values = self.parse_sat_obs_v2(sat.system, &obs_lines)?;
1422 sats.insert(sat, values);
1423 }
1424 self.epochs.push(ObsEpoch {
1425 epoch: epoch_time,
1426 flag,
1427 rcv_clock_offset_s,
1428 epoch_picoseconds: None,
1429 declared_record_count: numsat,
1430 special_record_count: 0,
1431 sats,
1432 });
1433 }
1434 Ok(())
1435 }
1436
1437 fn rinex2_obs_lines_per_sat(&self) -> Result<usize> {
1438 if self.rinex2_obs_codes.is_empty() {
1439 return Err(Error::Parse(
1440 "RINEX OBS header has no # / TYPES OF OBSERV records".into(),
1441 ));
1442 }
1443 Ok(self.rinex2_obs_codes.len().div_ceil(5))
1444 }
1445
1446 fn parse_sv_token_v2(&self, token: &str) -> Option<GnssSatelliteId> {
1447 parse_sv_token_v2(token, self.rinex2_default_system.unwrap_or(GnssSystem::Gps))
1448 }
1449
1450 fn ensure_rinex2_system_obs_codes(&mut self, system: GnssSystem) {
1451 self.obs_codes.entry(system).or_insert_with(|| {
1452 self.rinex2_obs_codes
1453 .iter()
1454 .map(|code| canonical_rinex2_obs_code(system, code))
1455 .collect()
1456 });
1457 }
1458
1459 fn parse_sat_obs_v2(&self, system: GnssSystem, obs_lines: &[String]) -> Result<Vec<ObsValue>> {
1460 let code_list = self.obs_codes.get(&system).ok_or_else(|| {
1461 Error::Parse(format!(
1462 "RINEX OBS satellite system {system} has no canonical observation-code table"
1463 ))
1464 })?;
1465 let mut values = Vec::with_capacity(code_list.len());
1466 for (i, code) in code_list.iter().enumerate() {
1467 let line = obs_lines.get(i / 5).map_or("", String::as_str);
1468 let start = (i % 5) * OBS_FIELD_WIDTH;
1469 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1470 let value = if value_str.is_empty() {
1471 None
1472 } else {
1473 let scale = self.scale_factor_for(system, code);
1474 let parsed = strict_f64_token(value_str, "observation.value", line)? / scale;
1475 if format!("{:.3}", parsed * scale).len() > OBS_VALUE_WIDTH {
1476 return Err(Error::Parse(
1477 "RINEX OBS observation value exceeds the F14.3 field width".into(),
1478 ));
1479 }
1480 Some(parsed)
1481 };
1482 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1483 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1484 values.push(ObsValue { value, lli, ssi });
1485 }
1486 Ok(values)
1487 }
1488
1489 fn collect_sat_record<'a, I: Iterator<Item = &'a str>>(
1490 &self,
1491 first_line: &str,
1492 lines: &mut std::iter::Peekable<I>,
1493 ) -> Result<String> {
1494 let first_line = ascii_fixed_columns(first_line);
1495 let token = field(&first_line, 0, 3);
1496 let sat = parse_sv_token(token).ok_or_else(|| {
1497 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1498 })?;
1499 let n_obs = self.obs_count_for_sat(sat)?;
1500 let mut record = first_line.into_owned();
1501
1502 while sat_record_field_count(record.len()) < n_obs {
1503 let Some(raw_next) = lines.peek().copied() else {
1504 break;
1505 };
1506 let next = raw_next.trim_end_matches(['\r', '\n']);
1507 let next = ascii_fixed_columns(next);
1508 if next.starts_with('>') || starts_with_sat_designator(&next) {
1515 break;
1516 }
1517 let continuation = lines.next().expect("peeked continuation line");
1518 let continuation = ascii_fixed_columns(continuation.trim_end_matches(['\r', '\n']));
1519 append_sat_continuation(&mut record, &continuation, n_obs);
1520 }
1521
1522 Ok(record)
1523 }
1524
1525 fn obs_count_for_sat(&self, sat: GnssSatelliteId) -> Result<usize> {
1526 self.obs_codes
1527 .get(&sat.system)
1528 .map(Vec::len)
1529 .ok_or_else(|| {
1530 Error::Parse(format!(
1531 "RINEX OBS satellite {sat} uses undeclared observation system"
1532 ))
1533 })
1534 }
1535
1536 fn parse_sat_line(&self, line: &str) -> Result<(GnssSatelliteId, Vec<ObsValue>)> {
1537 let token = field(line, 0, 3);
1538 let sat = parse_sv_token(token).ok_or_else(|| {
1539 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1540 })?;
1541 let code_list = self.obs_codes.get(&sat.system).ok_or_else(|| {
1542 Error::Parse(format!(
1543 "RINEX OBS satellite {sat} uses undeclared observation system"
1544 ))
1545 })?;
1546 let mut values = Vec::with_capacity(code_list.len());
1547 for (i, code) in code_list.iter().enumerate() {
1548 let start = 3 + i * OBS_FIELD_WIDTH;
1549 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1550 let value = if value_str.is_empty() {
1551 None
1552 } else {
1553 let scale = self.scale_factor_for(sat.system, code);
1554 let parsed = strict_f64_token(value_str, "observation.value", line)? / scale;
1555 if format!("{:.3}", parsed * scale).len() > OBS_VALUE_WIDTH {
1562 return Err(Error::Parse(
1563 "RINEX OBS observation value exceeds the F14.3 field width".into(),
1564 ));
1565 }
1566 Some(parsed)
1567 };
1568 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1569 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1570 values.push(ObsValue { value, lli, ssi });
1571 }
1572 Ok((sat, values))
1573 }
1574
1575 fn finish(self) -> Result<RinexObs> {
1576 let version = self
1577 .version
1578 .ok_or_else(|| Error::Parse("RINEX OBS missing RINEX VERSION / TYPE".into()))?;
1579 if let Some(remaining) = self.glonass_slots_remaining {
1580 if remaining != 0 {
1581 return Err(Error::Parse(format!(
1582 "RINEX OBS GLONASS slot table missing {remaining} declared entries"
1583 )));
1584 }
1585 }
1586 let mut obs_codes = self.obs_codes;
1587 if obs_codes.is_empty() && !self.rinex2_obs_codes.is_empty() {
1588 let system = self.rinex2_default_system.unwrap_or(GnssSystem::Gps);
1589 obs_codes.insert(
1590 system,
1591 self.rinex2_obs_codes
1592 .iter()
1593 .map(|code| canonical_rinex2_obs_code(system, code))
1594 .collect(),
1595 );
1596 }
1597 if obs_codes.is_empty() {
1598 return Err(Error::Parse(
1599 "RINEX OBS header has no SYS / # / OBS TYPES records".into(),
1600 ));
1601 }
1602 let header = ObsHeader {
1603 version,
1604 approx_position_m: self.approx_position_m,
1605 antenna_delta_hen_m: self.antenna_delta_hen_m,
1606 obs_codes,
1607 program_run_by_date: self.program_run_by_date,
1608 comments: self.comments,
1609 marker_number: self.marker_number,
1610 marker_type: self.marker_type,
1611 observer: self.observer,
1612 agency: self.agency,
1613 receiver: self.receiver,
1614 antenna: self.antenna,
1615 interval_s: self.interval_s,
1616 time_of_first_obs: self.time_of_first_obs,
1617 time_of_last_obs: self.time_of_last_obs,
1618 n_satellites: self.n_satellites,
1619 prn_obs_counts: self.prn_obs_counts,
1620 phase_shifts: self.phase_shifts,
1621 scale_factors: self.scale_factors,
1622 glonass_slots: self.glonass_slots,
1623 glonass_cod_phs_bis: self.glonass_cod_phs_bis,
1624 signal_strength_unit: self.signal_strength_unit,
1625 leap_seconds: self.leap_seconds,
1626 marker_name: self.marker_name,
1627 unretained_header_labels: self.unretained_header_labels,
1628 };
1629 Ok(RinexObs {
1630 header,
1631 epochs: self.epochs,
1632 skipped_records: self.diagnostics.skips.len(),
1633 })
1634 }
1635
1636 fn scale_factor_for(&self, system: GnssSystem, code: &str) -> f64 {
1637 self.scale_factors
1638 .iter()
1639 .rev()
1640 .find(|record| {
1641 record.system == system
1642 && (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
1643 })
1644 .map_or(1.0, |record| record.factor)
1645 }
1646}
1647
1648type ParsedEpochLine = (ObsEpochTime, u8, usize, Option<f64>, Option<u32>);
1651
1652fn parse_epoch_line(
1653 line: &str,
1654 second_policy: validate::CivilSecondPolicy,
1655) -> Result<ParsedEpochLine> {
1656 let body = line
1657 .strip_prefix('>')
1658 .ok_or_else(|| Error::Parse(format!("RINEX OBS epoch line lacks '>': {line:?}")))?;
1659 let tokens: Vec<&str> = body.split_whitespace().collect();
1660 if tokens.len() < 8 {
1661 return Err(Error::Parse(format!(
1662 "RINEX OBS epoch line has too few fields in {line:?}"
1663 )));
1664 }
1665 let epoch = parse_epoch_time_tokens(
1666 &tokens[..6].join(" "),
1667 line,
1668 [
1669 "epoch.year",
1670 "epoch.month",
1671 "epoch.day",
1672 "epoch.hour",
1673 "epoch.minute",
1674 "epoch.second",
1675 ],
1676 second_policy,
1677 )?;
1678
1679 let mut index = 6;
1680 let epoch_picoseconds = if tokens
1681 .get(index)
1682 .is_some_and(|token| token.len() == 5 && token.bytes().all(|b| b.is_ascii_digit()))
1683 && tokens.len() >= 9
1684 {
1685 let value = strict_int_token::<u32>(tokens[index], "epoch.picoseconds", line)?;
1686 index += 1;
1687 Some(value)
1688 } else {
1689 None
1690 };
1691 let flag = strict_int_token::<u8>(tokens[index], "epoch.flag", line)?;
1692 index += 1;
1693 let numsat = strict_int_token::<usize>(tokens[index], "epoch.satellite_count", line)?;
1694 index += 1;
1695 let rcv_clock_offset_s = tokens
1696 .get(index)
1697 .map(|token| strict_f64_token(token, "epoch.rcv_clock_offset_s", line))
1698 .transpose()?;
1699 Ok((epoch, flag, numsat, rcv_clock_offset_s, epoch_picoseconds))
1700}
1701
1702type ParsedEpochLineV2 = (ObsEpochTime, u8, usize, Option<f64>);
1703
1704fn parse_epoch_line_v2(
1705 line: &str,
1706 second_policy: validate::CivilSecondPolicy,
1707) -> Result<ParsedEpochLineV2> {
1708 let head = field(line, 0, 32);
1709 let tokens: Vec<&str> = head.split_whitespace().collect();
1710 if tokens.len() < 8 {
1711 return Err(Error::Parse(format!(
1712 "RINEX OBS v2 epoch line has too few fields in {line:?}"
1713 )));
1714 }
1715 let year = strict_int_token::<i32>(tokens[0], "epoch.year", line)?;
1716 let year = expand_rinex2_year(year);
1717 let month = strict_int_token::<i64>(tokens[1], "epoch.month", line)?;
1718 let day = strict_int_token::<i64>(tokens[2], "epoch.day", line)?;
1719 let hour = strict_int_token::<i64>(tokens[3], "epoch.hour", line)?;
1720 let minute = strict_int_token::<i64>(tokens[4], "epoch.minute", line)?;
1721 let second = strict_f64_token(tokens[5], "epoch.second", line)?;
1722 let civil = validate::civil_datetime_with_second_policy(
1723 i64::from(year),
1724 month,
1725 day,
1726 hour,
1727 minute,
1728 second,
1729 second_policy,
1730 )
1731 .map_err(|error| map_field_error(error, line))?;
1732 let flag = strict_int_token::<u8>(tokens[6], "epoch.flag", line)?;
1733 let numsat = strict_int_token::<usize>(tokens[7], "epoch.satellite_count", line)?;
1734 let clock = field(line, 68, line.len()).trim();
1735 let rcv_clock_offset_s = if clock.is_empty() {
1736 None
1737 } else {
1738 Some(strict_f64_token(clock, "epoch.rcv_clock_offset_s", line)?)
1739 };
1740 Ok((
1741 ObsEpochTime {
1742 year,
1743 month: civil.month as u8,
1744 day: civil.day as u8,
1745 hour: civil.hour as u8,
1746 minute: civil.minute as u8,
1747 second: civil.second,
1748 },
1749 flag,
1750 numsat,
1751 rcv_clock_offset_s,
1752 ))
1753}
1754
1755fn expand_rinex2_year(year: i32) -> i32 {
1756 if year >= 100 {
1757 year
1758 } else if year >= 80 {
1759 1900 + year
1760 } else {
1761 2000 + year
1762 }
1763}
1764
1765fn collect_epoch_sv_tokens_v2<'a, I: Iterator<Item = &'a str>>(
1766 first_line: &str,
1767 count: usize,
1768 lines: &mut std::iter::Peekable<I>,
1769) -> Result<Vec<String>> {
1770 let mut tokens = Vec::with_capacity(count);
1771 append_epoch_sv_tokens_v2(first_line, count, &mut tokens);
1772 while tokens.len() < count {
1773 let continuation = lines.next().ok_or_else(|| {
1774 Error::Parse("RINEX OBS v2 epoch truncated: missing satellite-list line".into())
1775 })?;
1776 append_epoch_sv_tokens_v2(
1777 continuation.trim_end_matches(['\r', '\n']),
1778 count,
1779 &mut tokens,
1780 );
1781 }
1782 tokens.truncate(count);
1783 Ok(tokens)
1784}
1785
1786fn append_epoch_sv_tokens_v2(line: &str, count: usize, tokens: &mut Vec<String>) {
1787 let remaining = count.saturating_sub(tokens.len());
1788 for i in 0..remaining.min(12) {
1789 let start = 32 + i * 3;
1790 let token = field(line, start, start + 3);
1791 if token.trim().is_empty() {
1792 break;
1793 }
1794 tokens.push(token.to_string());
1795 }
1796}
1797
1798fn parse_sv_token_v2(token: &str, default_system: GnssSystem) -> Option<GnssSatelliteId> {
1799 let token = token.trim();
1800 if token.is_empty() {
1801 return None;
1802 }
1803 let mut chars = token.chars();
1804 let first = chars.next()?;
1805 let (system, prn_text) = if let Some(system) = GnssSystem::from_letter(first) {
1806 (system, chars.as_str().trim())
1807 } else {
1808 (default_system, token)
1809 };
1810 let prn = prn_text.parse::<u8>().ok()?;
1811 GnssSatelliteId::new(system, prn).ok()
1812}
1813
1814fn canonical_rinex2_obs_code(system: GnssSystem, code: &str) -> String {
1815 let code = code.trim();
1816 if code.len() == 3 {
1817 return code.to_string();
1818 }
1819 let mut chars = code.chars();
1820 let Some(kind) = chars.next() else {
1821 return code.to_string();
1822 };
1823 let Some(band) = chars.next() else {
1824 return code.to_string();
1825 };
1826 if chars.next().is_some() || !matches!(kind, 'C' | 'P' | 'L' | 'D' | 'S') {
1827 return code.to_string();
1828 }
1829
1830 if let Some(mapped) = canonical_rinex2_code_exact(system, kind, band) {
1831 return mapped.to_string();
1832 }
1833
1834 let canonical_kind = if kind == 'P' { 'C' } else { kind };
1835 let attr = rinex2_default_tracking_attr(system, kind, band);
1836 format!("{canonical_kind}{band}{attr}")
1837}
1838
1839fn canonical_rinex2_code_exact(system: GnssSystem, kind: char, band: char) -> Option<&'static str> {
1840 match (system, kind, band) {
1841 (GnssSystem::Gps, 'C', '1') => Some("C1C"),
1842 (GnssSystem::Gps, 'C', '2') => Some("C2C"),
1843 (GnssSystem::Gps, 'P', '1') => Some("C1W"),
1844 (GnssSystem::Gps, 'P', '2') => Some("C2W"),
1845 (GnssSystem::Glonass, 'C', '1') => Some("C1C"),
1846 (GnssSystem::Glonass, 'C', '2') => Some("C2C"),
1847 (GnssSystem::Glonass, 'P', '1') => Some("C1P"),
1848 (GnssSystem::Glonass, 'P', '2') => Some("C2P"),
1849 (GnssSystem::Galileo, 'C', '1') => Some("C1C"),
1850 (GnssSystem::Galileo, 'C', '2') => Some("C5Q"),
1851 (GnssSystem::Galileo, 'P', '1') => Some("C1X"),
1852 (GnssSystem::Galileo, 'P', '2') => Some("C5X"),
1853 (GnssSystem::BeiDou, 'C', '1') => Some("C2I"),
1854 (GnssSystem::BeiDou, 'C', '2') => Some("C7I"),
1855 (GnssSystem::BeiDou, 'P', '1') => Some("C2I"),
1856 (GnssSystem::BeiDou, 'P', '2') => Some("C6I"),
1857 (GnssSystem::Sbas, 'C', '1') => Some("C1C"),
1858 _ => None,
1859 }
1860}
1861
1862fn rinex2_default_tracking_attr(system: GnssSystem, kind: char, band: char) -> char {
1863 match system {
1864 GnssSystem::Gps => match band {
1865 '1' => 'C',
1866 '2' => {
1867 if kind == 'C' {
1868 'C'
1869 } else {
1870 'W'
1871 }
1872 }
1873 '5' => 'X',
1874 _ => 'X',
1875 },
1876 GnssSystem::Glonass => match band {
1877 '1' => 'C',
1878 '2' => 'P',
1879 '3' => 'X',
1880 _ => 'X',
1881 },
1882 GnssSystem::Galileo => match band {
1883 '1' | '6' => 'C',
1884 '5' | '7' | '8' => 'X',
1885 _ => 'X',
1886 },
1887 GnssSystem::BeiDou => match band {
1888 '2' | '6' | '7' => 'I',
1889 '1' => 'P',
1890 '5' | '8' => 'X',
1891 _ => 'X',
1892 },
1893 GnssSystem::Qzss => match band {
1894 '1' => 'C',
1895 '2' => 'L',
1896 '5' | '6' => 'X',
1897 _ => 'X',
1898 },
1899 GnssSystem::Navic => match band {
1900 '5' | '9' => 'A',
1901 _ => 'X',
1902 },
1903 GnssSystem::Sbas => match band {
1904 '1' => 'C',
1905 '5' => 'X',
1906 _ => 'X',
1907 },
1908 }
1909}
1910
1911fn time_scale_from_label(label: &str, line: &str) -> Result<TimeScale> {
1915 let label = label.trim();
1916 if label.is_empty() {
1917 Ok(TimeScale::Gpst)
1918 } else {
1919 time_scale_label(label).ok_or_else(|| {
1920 Error::Parse(format!(
1921 "RINEX OBS TIME OF FIRST OBS unknown time scale {label:?} in {line:?}"
1922 ))
1923 })
1924 }
1925}
1926
1927fn civil_second_policy_for_time_scale(scale: TimeScale) -> validate::CivilSecondPolicy {
1928 match scale {
1929 TimeScale::Utc | TimeScale::Glonasst => validate::CivilSecondPolicy::UtcLike,
1931 TimeScale::Tai
1932 | TimeScale::Tt
1933 | TimeScale::Tdb
1934 | TimeScale::Gpst
1935 | TimeScale::Gst
1936 | TimeScale::Bdt
1937 | TimeScale::Qzsst => validate::CivilSecondPolicy::Continuous,
1938 }
1939}
1940
1941fn parse_epoch_time_tokens(
1942 body: &str,
1943 line: &str,
1944 fields: [&'static str; 6],
1945 second_policy: validate::CivilSecondPolicy,
1946) -> Result<ObsEpochTime> {
1947 let tokens: Vec<&str> = body.split_whitespace().collect();
1948 if tokens.len() < fields.len() {
1949 let field = fields[tokens.len()];
1950 return Err(map_field_error(FieldError::Missing { field }, line));
1951 }
1952 let year = strict_int_token::<i32>(tokens[0], fields[0], line)?;
1953 let month = strict_int_token::<i64>(tokens[1], fields[1], line)?;
1954 let day = strict_int_token::<i64>(tokens[2], fields[2], line)?;
1955 let hour = strict_int_token::<i64>(tokens[3], fields[3], line)?;
1956 let minute = strict_int_token::<i64>(tokens[4], fields[4], line)?;
1957 let second = strict_f64_token(tokens[5], fields[5], line)?;
1958 let civil = validate::civil_datetime_with_second_policy(
1959 year as i64,
1960 month,
1961 day,
1962 hour,
1963 minute,
1964 second,
1965 second_policy,
1966 )
1967 .map_err(|error| map_field_error(error, line))?;
1968 Ok(ObsEpochTime {
1969 year,
1970 month: civil.month as u8,
1971 day: civil.day as u8,
1972 hour: civil.hour as u8,
1973 minute: civil.minute as u8,
1974 second: civil.second,
1975 })
1976}
1977
1978fn strict_vec3_tokens(body: &str, line: &str, fields: [&'static str; 3]) -> Result<[f64; 3]> {
1979 let tokens: Vec<&str> = body.split_whitespace().collect();
1980 if tokens.len() < fields.len() {
1981 let field = fields[tokens.len()];
1982 return Err(map_field_error(FieldError::Missing { field }, line));
1983 }
1984 Ok([
1985 strict_f64_token(tokens[0], fields[0], line)?,
1986 strict_f64_token(tokens[1], fields[1], line)?,
1987 strict_f64_token(tokens[2], fields[2], line)?,
1988 ])
1989}
1990
1991fn strict_f64_field(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<f64> {
1992 strict_f64_token(field(line, start, end), field_name, line)
1993}
1994
1995fn optional_i64_field(
1996 line: &str,
1997 start: usize,
1998 end: usize,
1999 field_name: &'static str,
2000) -> Result<Option<i64>> {
2001 let token = field(line, start, end).trim();
2002 if token.is_empty() {
2003 Ok(None)
2004 } else {
2005 strict_int_token::<i64>(token, field_name, line).map(Some)
2006 }
2007}
2008
2009fn optional_trimmed(line: &str, start: usize, end: usize) -> Option<String> {
2010 let value = field(line, start, end).trim();
2011 (!value.is_empty()).then(|| value.to_string())
2012}
2013
2014fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
2015where
2016 T: core::str::FromStr,
2017{
2018 strict_int_token(field(line, start, end), field_name, line)
2019}
2020
2021fn strict_f64_token(token: &str, field_name: &'static str, line: &str) -> Result<f64> {
2022 validate::strict_f64(token, field_name).map_err(|error| map_field_error(error, line))
2023}
2024
2025fn validate_finite_input(value: f64, field: &'static str) -> Result<()> {
2026 if value.is_finite() {
2027 Ok(())
2028 } else {
2029 Err(Error::InvalidInput(format!(
2030 "RINEX OBS {field} must be finite"
2031 )))
2032 }
2033}
2034
2035fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
2036where
2037 T: core::str::FromStr,
2038{
2039 validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
2040}
2041
2042fn scale_factor_value(value: u32) -> Result<f64> {
2043 match value {
2044 1 | 10 | 100 | 1000 => Ok(f64::from(value)),
2045 _ => Err(Error::Parse(format!(
2046 "RINEX OBS invalid scale_factor.factor: expected 1, 10, 100, or 1000, got {value}"
2047 ))),
2048 }
2049}
2050
2051fn map_field_error(error: FieldError, line: &str) -> Error {
2052 Error::Parse(format!(
2053 "RINEX OBS invalid {}: {error} in {line:?}",
2054 error.field()
2055 ))
2056}
2057
2058fn obs_payload_field_count(payload_len: usize) -> usize {
2059 let full = payload_len / OBS_FIELD_WIDTH;
2060 let trailing = payload_len % OBS_FIELD_WIDTH;
2061 full + usize::from(trailing >= OBS_VALUE_WIDTH)
2062}
2063
2064fn sat_record_field_count(record_len: usize) -> usize {
2065 obs_payload_field_count(record_len.saturating_sub(3))
2066}
2067
2068fn ascii_fixed_columns(line: &str) -> Cow<'_, str> {
2069 if line.is_ascii() {
2070 Cow::Borrowed(line)
2071 } else {
2072 Cow::Owned(
2073 line.chars()
2074 .map(|ch| if ch.is_ascii() { ch } else { ' ' })
2075 .collect(),
2076 )
2077 }
2078}
2079
2080fn truncate_to_char_boundary(record: &mut String, len: usize) {
2081 let mut end = len.min(record.len());
2082 while !record.is_char_boundary(end) {
2083 end -= 1;
2084 }
2085 record.truncate(end);
2086}
2087
2088fn starts_with_sat_designator(line: &str) -> bool {
2096 let b = line.as_bytes();
2097 b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1].is_ascii_digit() && b[2].is_ascii_digit()
2098}
2099
2100fn consume_skipped_sat_continuations<'a, I: Iterator<Item = &'a str>>(
2104 lines: &mut std::iter::Peekable<I>,
2105) {
2106 while let Some(raw_next) = lines.peek().copied() {
2107 let next = ascii_fixed_columns(raw_next.trim_end_matches(['\r', '\n']));
2108 if next.starts_with('>') || starts_with_sat_designator(&next) {
2109 break;
2110 }
2111 lines.next();
2112 }
2113}
2114
2115fn append_sat_continuation(record: &mut String, continuation: &str, n_obs: usize) {
2116 let fields_present = sat_record_field_count(record.len());
2117 let logical_len = 3 + fields_present * OBS_FIELD_WIDTH;
2118 truncate_to_char_boundary(record, logical_len);
2119
2120 let remaining = n_obs.saturating_sub(fields_present);
2121 let payload = field(continuation, 3, continuation.len());
2122 let fields_available = obs_payload_field_count(payload.len());
2123 let fields_to_copy = remaining.min(fields_available);
2124 let width = fields_to_copy * OBS_FIELD_WIDTH;
2125 record.push_str(field(payload, 0, width));
2126}
2127
2128fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
2130 token.parse::<GnssSatelliteId>().ok()
2131}
2132
2133fn digit_at(line: &str, col: usize) -> Option<u8> {
2136 line.as_bytes()
2137 .get(col)
2138 .filter(|b| b.is_ascii_digit())
2139 .map(|b| b - b'0')
2140}
2141
2142mod write;
2143
2144#[cfg(all(test, sidereon_repo_tests))]
2145mod tests;