1use std::borrow::Cow;
39use std::collections::BTreeMap;
40
41use crate::astro::time::model::TimeScale;
42
43use crate::format::columns::raw_field as field;
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)]
120pub struct ObsEpoch {
121 pub epoch: ObsEpochTime,
123 pub flag: u8,
125 pub sats: BTreeMap<GnssSatelliteId, Vec<ObsValue>>,
128}
129
130#[derive(Debug, Clone, PartialEq)]
132pub struct ObsHeader {
133 pub version: f64,
135 pub approx_position_m: Option<[f64; 3]>,
138 pub antenna_delta_hen_m: Option<[f64; 3]>,
142 pub obs_codes: BTreeMap<GnssSystem, Vec<String>>,
144 pub interval_s: Option<f64>,
146 pub time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
148 pub phase_shifts: Vec<ObsPhaseShift>,
150 pub scale_factors: Vec<ObsScaleFactor>,
152 pub glonass_slots: BTreeMap<u8, i8>,
154 pub marker_name: Option<String>,
156}
157
158#[derive(Debug, Clone, PartialEq)]
164pub struct RinexObs {
165 pub header: ObsHeader,
167 pub epochs: Vec<ObsEpoch>,
170 pub skipped_records: usize,
177}
178
179impl RinexObs {
180 pub fn parse(text: &str) -> Result<Self> {
186 let mut parser = Parser::new();
187 let mut lines = text.lines();
188 parser.parse_header(&mut lines)?;
189 parser.parse_body(&mut lines.peekable())?;
190 parser.finish()
191 }
192
193 pub fn header(&self) -> &ObsHeader {
195 &self.header
196 }
197
198 pub fn epochs(&self) -> &[ObsEpoch] {
200 &self.epochs
201 }
202
203 pub fn obs_codes(&self, sys: GnssSystem) -> Option<&[String]> {
205 self.header.obs_codes.get(&sys).map(Vec::as_slice)
206 }
207}
208
209impl core::str::FromStr for RinexObs {
210 type Err = Error;
211
212 fn from_str(s: &str) -> Result<Self> {
213 Self::parse(s)
214 }
215}
216
217#[derive(Debug, Clone, PartialEq)]
224pub struct SignalPolicy {
225 pub codes: BTreeMap<GnssSystem, Vec<String>>,
227}
228
229impl SignalPolicy {
230 pub fn default_for(version: f64) -> Result<Self> {
240 validate_finite_input(version, "version")?;
241 let mut codes = BTreeMap::new();
242 codes.insert(GnssSystem::Gps, vec!["C1C".to_string()]);
243 codes.insert(
244 GnssSystem::Galileo,
245 vec!["C1C".to_string(), "C1X".to_string()],
246 );
247 let beidou = if (3.015..3.025).contains(&version) {
252 vec!["C1I".to_string(), "C2I".to_string()]
253 } else {
254 vec!["C2I".to_string(), "C1I".to_string()]
255 };
256 codes.insert(GnssSystem::BeiDou, beidou);
257 codes.insert(GnssSystem::Glonass, vec!["C1C".to_string()]);
258 Ok(Self { codes })
259 }
260
261 pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
263 self.codes.insert(sys, codes);
264 self
265 }
266}
267
268#[derive(Debug, Clone, Default, PartialEq, Eq)]
274pub struct ObservationFilter {
275 pub codes: BTreeMap<GnssSystem, Vec<String>>,
277}
278
279impl ObservationFilter {
280 pub fn all() -> Self {
282 Self::default()
283 }
284
285 pub fn from_entries<I>(entries: I) -> Self
287 where
288 I: IntoIterator<Item = (GnssSystem, Vec<String>)>,
289 {
290 Self {
291 codes: entries.into_iter().collect(),
292 }
293 }
294
295 fn allowed_codes(&self, system: GnssSystem) -> Option<&[String]> {
296 if self.codes.is_empty() {
297 Some(&[])
298 } else {
299 self.codes.get(&system).map(Vec::as_slice)
300 }
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum ObservationKind {
307 Pseudorange,
309 CarrierPhase,
311 Doppler,
313 SignalStrength,
315 Unknown,
317}
318
319impl ObservationKind {
320 pub fn from_code(code: &str) -> Self {
322 match code.as_bytes().first().copied() {
323 Some(b'C') => Self::Pseudorange,
324 Some(b'L') => Self::CarrierPhase,
325 Some(b'D') => Self::Doppler,
326 Some(b'S') => Self::SignalStrength,
327 _ => Self::Unknown,
328 }
329 }
330
331 pub fn as_str(self) -> &'static str {
333 match self {
334 Self::Pseudorange => "pseudorange",
335 Self::CarrierPhase => "carrier_phase",
336 Self::Doppler => "doppler",
337 Self::SignalStrength => "signal_strength",
338 Self::Unknown => "unknown",
339 }
340 }
341
342 pub fn units_str(self) -> &'static str {
344 match self {
345 Self::Pseudorange => "meters",
346 Self::CarrierPhase => "cycles",
347 Self::Doppler => "hz",
348 Self::SignalStrength => "db_hz",
349 Self::Unknown => "unknown",
350 }
351 }
352}
353
354#[derive(Debug, Clone, PartialEq)]
356pub struct ObservationValueRow {
357 pub code: String,
359 pub kind: ObservationKind,
361 pub value: Option<f64>,
363 pub lli: Option<u8>,
365 pub ssi: Option<u8>,
367}
368
369#[derive(Debug, Clone, PartialEq)]
371pub struct CarrierPhaseRow {
372 pub code: String,
374 pub value_cycles: Option<f64>,
376 pub lli: Option<u8>,
378 pub ssi: Option<u8>,
380 pub frequency_hz: Option<f64>,
382 pub wavelength_m: Option<f64>,
384 pub value_m: Option<f64>,
386 pub phase_shift_cycles: f64,
390}
391
392pub fn observation_values(
394 obs: &RinexObs,
395 epoch: &ObsEpoch,
396 filter: &ObservationFilter,
397) -> Result<Vec<(GnssSatelliteId, Vec<ObservationValueRow>)>> {
398 let mut out = Vec::new();
399 for (sat, values) in epoch
400 .sats
401 .iter()
402 .filter(|(sat, _)| filter.allowed_codes(sat.system).is_some())
403 {
404 let allowed_codes = filter
405 .allowed_codes(sat.system)
406 .expect("filter presence checked");
407 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
408 continue;
409 };
410 let mut rows = Vec::new();
411 for (code, value) in code_list.iter().zip(values.iter()) {
412 if !allowed_codes.is_empty() && !allowed_codes.iter().any(|c| c == code) {
413 continue;
414 }
415 if let Some(value) = value.value {
416 validate_finite_input(value, "observation.value")?;
417 }
418 let kind = ObservationKind::from_code(code);
419 rows.push(ObservationValueRow {
420 code: code.clone(),
421 kind,
422 value: value.value,
423 lli: value.lli,
424 ssi: value.ssi,
425 });
426 }
427 out.push((*sat, rows));
428 }
429 Ok(out)
430}
431
432pub fn carrier_phase_rows(
434 obs: &RinexObs,
435 epoch: &ObsEpoch,
436 filter: &ObservationFilter,
437) -> Result<Vec<(GnssSatelliteId, Vec<CarrierPhaseRow>)>> {
438 validate_finite_input(obs.header.version, "version")?;
439 let mut out = Vec::new();
440 for (sat, rows) in observation_values(obs, epoch, filter)? {
441 let phases = rows
442 .into_iter()
443 .filter(|row| row.kind == ObservationKind::CarrierPhase)
444 .map(|row| carrier_phase_row(obs, sat, row))
445 .collect::<Result<Vec<_>>>()?;
446 out.push((sat, phases));
447 }
448 Ok(out)
449}
450
451pub fn band_frequency_hz(
456 system: GnssSystem,
457 band: char,
458 glonass_channel: Option<i8>,
459) -> Option<f64> {
460 rinex_band_frequency_hz(system, band, glonass_channel)
461}
462
463pub fn observation_frequency_hz(
465 system: GnssSystem,
466 code: &str,
467 rinex_version: f64,
468 glonass_channel: Option<i8>,
469) -> Result<Option<f64>> {
470 validate_finite_input(rinex_version, "version")?;
471 Ok(rinex_observation_frequency_hz(
472 system,
473 code,
474 rinex_version,
475 glonass_channel,
476 ))
477}
478
479fn carrier_phase_row(
480 obs: &RinexObs,
481 sat: GnssSatelliteId,
482 row: ObservationValueRow,
483) -> Result<CarrierPhaseRow> {
484 let glonass_channel = obs.header.glonass_slots.get(&sat.prn).copied();
485 let frequency_hz =
486 observation_frequency_hz(sat.system, &row.code, obs.header.version, glonass_channel)?;
487 let phase_shift_cycles = phase_shift_cycles(obs, sat, &row.code);
488 let value_cycles = row.value;
489 let wavelength_m =
490 rinex_observation_wavelength_m(sat.system, &row.code, obs.header.version, glonass_channel);
491 let value_m = match value_cycles.zip(wavelength_m) {
492 Some((cycles, lambda)) => {
493 let value_m = cycles * lambda;
494 validate_finite_input(value_m, "carrier_phase.value_m")?;
495 Some(value_m)
496 }
497 None => None,
498 };
499 Ok(CarrierPhaseRow {
500 code: row.code,
501 value_cycles,
502 lli: row.lli,
503 ssi: row.ssi,
504 frequency_hz,
505 wavelength_m,
506 value_m,
507 phase_shift_cycles,
508 })
509}
510
511fn phase_shift_cycles(obs: &RinexObs, sat: GnssSatelliteId, code: &str) -> f64 {
512 let mut system_wide = None;
513 for shift in obs.header.phase_shifts.iter().rev() {
514 if shift.system != sat.system || shift.code != code {
515 continue;
516 }
517 if shift.satellites.is_empty() {
518 if system_wide.is_none() {
519 system_wide = Some(shift.correction_cycles);
520 }
521 } else if shift.satellites.contains(&sat) {
522 return shift.correction_cycles;
523 }
524 }
525 system_wide.unwrap_or(0.0)
526}
527
528pub fn pseudoranges(
535 obs: &RinexObs,
536 epoch: &ObsEpoch,
537 policy: &SignalPolicy,
538) -> Result<Vec<(GnssSatelliteId, f64)>> {
539 let mut out = Vec::new();
540 for (sat, values) in &epoch.sats {
541 let Some(prefs) = policy.codes.get(&sat.system) else {
542 continue;
543 };
544 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
545 continue;
546 };
547 for code in prefs {
548 if let Some(idx) = code_list.iter().position(|c| c == code) {
549 if let Some(ObsValue {
550 value: Some(range_m),
551 ..
552 }) = values.get(idx)
553 {
554 validate_finite_input(*range_m, "pseudorange_m")?;
555 out.push((*sat, *range_m));
556 break;
557 }
558 }
559 }
560 }
561 Ok(out)
562}
563
564struct Parser {
566 version: Option<f64>,
567 is_observation: bool,
568 approx_position_m: Option<[f64; 3]>,
569 antenna_delta_hen_m: Option<[f64; 3]>,
570 obs_codes: BTreeMap<GnssSystem, Vec<String>>,
571 interval_s: Option<f64>,
572 time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
573 phase_shifts: Vec<ObsPhaseShift>,
574 scale_factors: Vec<ObsScaleFactor>,
575 scale_factor_continuation: Option<ScaleFactorContinuation>,
576 glonass_slots: BTreeMap<u8, i8>,
577 glonass_slots_remaining: Option<usize>,
578 marker_name: Option<String>,
579 epochs: Vec<ObsEpoch>,
580 current_obs_sys: Option<GnssSystem>,
583 obs_codes_remaining: usize,
585 diagnostics: Diagnostics,
590}
591
592#[derive(Debug, Clone, Copy)]
593struct ScaleFactorContinuation {
594 remaining: usize,
595}
596
597impl Parser {
598 fn new() -> Self {
599 Self {
600 version: None,
601 is_observation: false,
602 approx_position_m: None,
603 antenna_delta_hen_m: None,
604 obs_codes: BTreeMap::new(),
605 interval_s: None,
606 time_of_first_obs: None,
607 phase_shifts: Vec::new(),
608 scale_factors: Vec::new(),
609 scale_factor_continuation: None,
610 glonass_slots: BTreeMap::new(),
611 glonass_slots_remaining: None,
612 marker_name: None,
613 epochs: Vec::new(),
614 current_obs_sys: None,
615 obs_codes_remaining: 0,
616 diagnostics: Diagnostics::new(),
617 }
618 }
619
620 fn push_unrepresentable_satellite_skip(&mut self, token: &str) {
623 self.diagnostics.push_skip(Skip {
624 at: RecordRef::default().with_satellite(token.trim()),
625 reason: SkipReason::UnrepresentableSatellite,
626 });
627 }
628
629 fn parse_header<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut I) -> Result<()> {
630 let mut saw_end = false;
631 for raw in lines.by_ref() {
632 let line = raw.trim_end_matches(['\r', '\n']);
633 let label = field(line, 60, 80).trim();
634 match label {
635 "RINEX VERSION / TYPE" => self.parse_version(line)?,
636 "APPROX POSITION XYZ" => self.parse_approx_position(line)?,
637 "ANTENNA: DELTA H/E/N" => self.parse_antenna_delta(line)?,
638 "SYS / # / OBS TYPES" => self.parse_obs_types(line)?,
639 "SYS / SCALE FACTOR" => self.parse_scale_factor(line)?,
640 "SYS / PHASE SHIFT" => self.parse_phase_shift(line)?,
641 "TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
642 "INTERVAL" => {
643 self.interval_s = Some(strict_f64_field(line, 0, 10, "interval_s")?);
644 }
645 "GLONASS SLOT / FRQ #" => self.parse_glonass_slots(line)?,
646 "MARKER NAME" => {
647 let name = field(line, 0, 60).trim();
648 if !name.is_empty() {
649 self.marker_name = Some(name.to_string());
650 }
651 }
652 "END OF HEADER" => {
653 self.ensure_obs_type_count_complete(line)?;
654 self.ensure_scale_factor_count_complete(line)?;
655 saw_end = true;
656 break;
657 }
658 _ => {}
660 }
661 }
662 if !saw_end {
663 return Err(Error::Parse("RINEX OBS header has no END OF HEADER".into()));
664 }
665 Ok(())
666 }
667
668 fn parse_version(&mut self, line: &str) -> Result<()> {
669 let version = field(line, 0, 20).trim();
670 let version = strict_f64_token(version, "version", line)?;
671 let type_field = field(line, 20, 40);
673 self.is_observation =
674 type_field.trim_start().starts_with('O') || type_field.contains("OBSERVATION");
675 if !self.is_observation {
676 return Err(Error::Parse(format!(
677 "RINEX file is not observation data: {type_field:?}"
678 )));
679 }
680 if version.floor() as i64 != 3 {
681 return Err(Error::Parse(format!(
682 "RINEX OBS parser requires major version 3, got {version}"
683 )));
684 }
685 self.version = Some(version);
686 Ok(())
687 }
688
689 fn parse_approx_position(&mut self, line: &str) -> Result<()> {
690 let body = field(line, 0, 60);
691 self.approx_position_m = Some(strict_vec3_tokens(
692 body,
693 line,
694 [
695 "approx_position.x_m",
696 "approx_position.y_m",
697 "approx_position.z_m",
698 ],
699 )?);
700 Ok(())
701 }
702
703 fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
704 let body = field(line, 0, 60);
705 self.antenna_delta_hen_m = Some(strict_vec3_tokens(
706 body,
707 line,
708 [
709 "antenna_delta.height_m",
710 "antenna_delta.east_m",
711 "antenna_delta.north_m",
712 ],
713 )?);
714 Ok(())
715 }
716
717 fn parse_obs_types(&mut self, line: &str) -> Result<()> {
718 let sys_field = field(line, 0, 1).trim();
722 if !sys_field.is_empty() {
723 self.ensure_obs_type_count_complete(line)?;
724 let letter = sys_field.chars().next().unwrap();
725 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
726 Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
727 })?;
728 let count = strict_int_field::<usize>(line, 3, 6, "obs_type_count")?;
729 self.current_obs_sys = Some(system);
730 self.obs_codes_remaining = count;
731 self.obs_codes.entry(system).or_default();
732 }
733 let Some(system) = self.current_obs_sys else {
734 return Ok(());
735 };
736 let codes_section = field(line, 7, 60);
739 let list = self.obs_codes.get_mut(&system).expect("system inserted");
740 for tok in codes_section.split_whitespace() {
741 if self.obs_codes_remaining == 0 {
742 return Err(Error::Parse(format!(
743 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
744 )));
745 }
746 list.push(tok.to_string());
747 self.obs_codes_remaining -= 1;
748 }
749 Ok(())
750 }
751
752 fn ensure_obs_type_count_complete(&self, line: &str) -> Result<()> {
753 if self.obs_codes_remaining == 0 {
754 return Ok(());
755 }
756 let Some(system) = self.current_obs_sys else {
757 return Ok(());
758 };
759 let supplied = self.obs_codes.get(&system).map_or(0, Vec::len);
760 let declared = supplied + self.obs_codes_remaining;
761 Err(Error::Parse(format!(
762 "RINEX OBS {system} SYS / # / OBS TYPES declares {declared} codes but supplies {supplied} before {line:?}"
763 )))
764 }
765
766 fn parse_phase_shift(&mut self, line: &str) -> Result<()> {
767 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
768 if tokens.is_empty() {
769 return Ok(());
770 }
771 if tokens.len() < 2 {
772 return Err(Error::Parse(format!(
773 "RINEX OBS phase-shift header has too few fields in {line:?}"
774 )));
775 }
776
777 let system = tokens[0]
778 .chars()
779 .next()
780 .and_then(GnssSystem::from_letter)
781 .ok_or_else(|| {
782 Error::Parse(format!(
783 "RINEX OBS phase-shift system unparsable in {line:?}"
784 ))
785 })?;
786 let code = tokens[1].to_string();
787 let correction_cycles = match tokens.get(2) {
788 Some(token) => strict_f64_token(token, "phase_shift.correction_cycles", line)?,
789 None => 0.0,
790 };
791
792 let satellites = if let Some(count_token) = tokens.get(3) {
793 let count =
794 strict_int_token::<usize>(count_token, "phase_shift.satellite_count", line)?;
795 let sat_tokens = &tokens[4..];
796 if sat_tokens.len() != count {
797 return Err(Error::Parse(format!(
798 "RINEX OBS phase-shift satellite count mismatch in {line:?}"
799 )));
800 }
801 sat_tokens
802 .iter()
803 .map(|token| {
804 parse_sv_token(token).ok_or_else(|| {
805 Error::Parse(format!(
806 "RINEX OBS phase-shift satellite token {token:?} unparsable in {line:?}"
807 ))
808 })
809 })
810 .collect::<Result<Vec<_>>>()?
811 } else {
812 Vec::new()
813 };
814
815 self.phase_shifts.push(ObsPhaseShift {
816 system,
817 code,
818 correction_cycles,
819 satellites,
820 });
821 Ok(())
822 }
823
824 fn parse_scale_factor(&mut self, line: &str) -> Result<()> {
825 let sys_field = field(line, 0, 1).trim();
826 if !sys_field.is_empty() {
827 self.ensure_scale_factor_count_complete(line)?;
828 let letter = sys_field.chars().next().unwrap();
829 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
830 Error::Parse(format!("RINEX OBS unknown scale-factor system {letter:?}"))
831 })?;
832 let factor =
833 scale_factor_value(strict_int_field::<u32>(line, 2, 6, "scale_factor.factor")?)?;
834 let count_field = field(line, 8, 10).trim();
835 let count = if count_field.is_empty() {
836 0
837 } else {
838 strict_int_token::<usize>(count_field, "scale_factor.obs_type_count", line)?
839 };
840 self.scale_factors.push(ObsScaleFactor {
841 system,
842 factor,
843 codes: Vec::new(),
844 });
845 if count == 0 {
846 return Ok(());
847 }
848 self.scale_factor_continuation = Some(ScaleFactorContinuation { remaining: count });
849 }
850
851 self.collect_scale_factor_codes(line)
852 }
853
854 fn collect_scale_factor_codes(&mut self, line: &str) -> Result<()> {
855 let Some(mut continuation) = self.scale_factor_continuation else {
856 return Ok(());
857 };
858 let record = self
859 .scale_factors
860 .last_mut()
861 .expect("scale factor continuation has a record");
862 for code in field(line, 10, 60).split_whitespace() {
863 if continuation.remaining == 0 {
864 return Err(Error::Parse(format!(
865 "RINEX OBS SYS / SCALE FACTOR lists more codes than declared in {line:?}"
866 )));
867 }
868 record.codes.push(code.to_string());
869 continuation.remaining -= 1;
870 }
871 self.scale_factor_continuation = (continuation.remaining > 0).then_some(continuation);
872 Ok(())
873 }
874
875 fn ensure_scale_factor_count_complete(&self, line: &str) -> Result<()> {
876 let Some(continuation) = self.scale_factor_continuation else {
877 return Ok(());
878 };
879 let supplied = self
880 .scale_factors
881 .last()
882 .map_or(0, |record| record.codes.len());
883 let declared = supplied + continuation.remaining;
884 Err(Error::Parse(format!(
885 "RINEX OBS SYS / SCALE FACTOR declares {declared} codes but supplies {supplied} before {line:?}"
886 )))
887 }
888
889 fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
890 let body = field(line, 0, 43);
891 let scale_label = field(line, 48, 51).trim();
892 let scale = time_scale_from_label(scale_label, line)?;
893 let epoch = parse_epoch_time_tokens(
894 body,
895 line,
896 [
897 "time_of_first_obs.year",
898 "time_of_first_obs.month",
899 "time_of_first_obs.day",
900 "time_of_first_obs.hour",
901 "time_of_first_obs.minute",
902 "time_of_first_obs.second",
903 ],
904 civil_second_policy_for_time_scale(scale),
905 )?;
906 self.time_of_first_obs = Some((epoch, scale));
907 Ok(())
908 }
909
910 fn parse_glonass_slots(&mut self, line: &str) -> Result<()> {
911 let count_field = field(line, 0, 3).trim();
913 if !count_field.is_empty() {
914 let count = strict_int_token::<usize>(count_field, "glonass_slot.count", line)?;
915 self.glonass_slots_remaining = Some(count);
916 }
917 let body = field(line, 4, 60);
918 let tokens: Vec<&str> = body.split_whitespace().collect();
919 if !tokens.len().is_multiple_of(2) {
920 return Err(Error::Parse(format!(
921 "RINEX OBS GLONASS slot table has an odd token count in {line:?}"
922 )));
923 }
924 for pair in tokens.chunks_exact(2) {
925 if let Some(remaining) = self.glonass_slots_remaining.as_mut() {
929 if *remaining == 0 {
930 return Err(Error::Parse(format!(
931 "RINEX OBS GLONASS slot table has more entries than declared in {line:?}"
932 )));
933 }
934 *remaining -= 1;
935 }
936 let Some(sat) = parse_sv_token(pair[0]) else {
942 self.push_unrepresentable_satellite_skip(pair[0]);
943 continue;
944 };
945 if sat.system != GnssSystem::Glonass {
946 return Err(Error::Parse(format!(
947 "RINEX OBS GLONASS slot token {:?} is not GLONASS in {line:?}",
948 pair[0]
949 )));
950 }
951 let channel = strict_int_token::<i8>(pair[1], "glonass_slot.channel", line)?;
952 if !valid_glonass_frequency_channel(i32::from(channel)) {
953 return Err(Error::Parse(format!(
954 "RINEX OBS invalid glonass_slot.channel: {channel} out of range in {line:?}"
955 )));
956 }
957 self.glonass_slots.insert(sat.prn, channel);
958 }
959 Ok(())
960 }
961
962 fn parse_body<'a, I: Iterator<Item = &'a str>>(
963 &mut self,
964 lines: &mut std::iter::Peekable<I>,
965 ) -> Result<()> {
966 while let Some(raw) = lines.next() {
967 let line = raw.trim_end_matches(['\r', '\n']);
968 if line.is_empty() {
969 continue;
970 }
971 if !line.starts_with('>') {
972 continue;
974 }
975 let time_scale = self
976 .time_of_first_obs
977 .map_or(TimeScale::Gpst, |(_, scale)| scale);
978 let (epoch_time, flag, numsat) =
979 parse_epoch_line(line, civil_second_policy_for_time_scale(time_scale))?;
980
981 if flag > 1 {
982 for _ in 0..numsat {
986 lines
987 .next()
988 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
989 }
990 self.epochs.push(ObsEpoch {
991 epoch: epoch_time,
992 flag,
993 sats: BTreeMap::new(),
994 });
995 continue;
996 }
997
998 let mut sats = BTreeMap::new();
999 for _ in 0..numsat {
1000 let sat_line = lines.next().ok_or_else(|| {
1001 Error::Parse("RINEX OBS epoch truncated: missing satellite line".into())
1002 })?;
1003 let sat_line = sat_line.trim_end_matches(['\r', '\n']);
1004 let normalized = ascii_fixed_columns(sat_line);
1011 if !starts_with_sat_designator(&normalized) {
1012 return Err(Error::Parse(
1017 "RINEX OBS epoch truncated: expected satellite record".into(),
1018 ));
1019 }
1020 if parse_sv_token(field(&normalized, 0, 3)).is_none() {
1021 self.push_unrepresentable_satellite_skip(field(&normalized, 0, 3));
1026 consume_skipped_sat_continuations(lines);
1027 continue;
1028 }
1029 let sat_record = self.collect_sat_record(sat_line, lines)?;
1030 let (sat, values) = self.parse_sat_line(&sat_record)?;
1031 sats.insert(sat, values);
1032 }
1033 self.epochs.push(ObsEpoch {
1034 epoch: epoch_time,
1035 flag,
1036 sats,
1037 });
1038 }
1039 Ok(())
1040 }
1041
1042 fn collect_sat_record<'a, I: Iterator<Item = &'a str>>(
1043 &self,
1044 first_line: &str,
1045 lines: &mut std::iter::Peekable<I>,
1046 ) -> Result<String> {
1047 let first_line = ascii_fixed_columns(first_line);
1048 let token = field(&first_line, 0, 3);
1049 let sat = parse_sv_token(token).ok_or_else(|| {
1050 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1051 })?;
1052 let n_obs = self.obs_count_for_sat(sat)?;
1053 let mut record = first_line.into_owned();
1054
1055 while sat_record_field_count(record.len()) < n_obs {
1056 let Some(raw_next) = lines.peek().copied() else {
1057 break;
1058 };
1059 let next = raw_next.trim_end_matches(['\r', '\n']);
1060 let next = ascii_fixed_columns(next);
1061 if next.starts_with('>') || starts_with_sat_designator(&next) {
1068 break;
1069 }
1070 let continuation = lines.next().expect("peeked continuation line");
1071 let continuation = ascii_fixed_columns(continuation.trim_end_matches(['\r', '\n']));
1072 append_sat_continuation(&mut record, &continuation, n_obs);
1073 }
1074
1075 Ok(record)
1076 }
1077
1078 fn obs_count_for_sat(&self, sat: GnssSatelliteId) -> Result<usize> {
1079 self.obs_codes
1080 .get(&sat.system)
1081 .map(Vec::len)
1082 .ok_or_else(|| {
1083 Error::Parse(format!(
1084 "RINEX OBS satellite {sat} uses undeclared observation system"
1085 ))
1086 })
1087 }
1088
1089 fn parse_sat_line(&self, line: &str) -> Result<(GnssSatelliteId, Vec<ObsValue>)> {
1090 let token = field(line, 0, 3);
1091 let sat = parse_sv_token(token).ok_or_else(|| {
1092 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1093 })?;
1094 let code_list = self.obs_codes.get(&sat.system).ok_or_else(|| {
1095 Error::Parse(format!(
1096 "RINEX OBS satellite {sat} uses undeclared observation system"
1097 ))
1098 })?;
1099 let mut values = Vec::with_capacity(code_list.len());
1100 for (i, code) in code_list.iter().enumerate() {
1101 let start = 3 + i * OBS_FIELD_WIDTH;
1102 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1103 let value = if value_str.is_empty() {
1104 None
1105 } else {
1106 let scale = self.scale_factor_for(sat.system, code);
1107 let parsed = strict_f64_token(value_str, "observation.value", line)? / scale;
1108 if format!("{:.3}", parsed * scale).len() > OBS_VALUE_WIDTH {
1115 return Err(Error::Parse(
1116 "RINEX OBS observation value exceeds the F14.3 field width".into(),
1117 ));
1118 }
1119 Some(parsed)
1120 };
1121 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1122 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1123 values.push(ObsValue { value, lli, ssi });
1124 }
1125 Ok((sat, values))
1126 }
1127
1128 fn finish(self) -> Result<RinexObs> {
1129 let version = self
1130 .version
1131 .ok_or_else(|| Error::Parse("RINEX OBS missing RINEX VERSION / TYPE".into()))?;
1132 if let Some(remaining) = self.glonass_slots_remaining {
1133 if remaining != 0 {
1134 return Err(Error::Parse(format!(
1135 "RINEX OBS GLONASS slot table missing {remaining} declared entries"
1136 )));
1137 }
1138 }
1139 if self.obs_codes.is_empty() {
1140 return Err(Error::Parse(
1141 "RINEX OBS header has no SYS / # / OBS TYPES records".into(),
1142 ));
1143 }
1144 let header = ObsHeader {
1145 version,
1146 approx_position_m: self.approx_position_m,
1147 antenna_delta_hen_m: self.antenna_delta_hen_m,
1148 obs_codes: self.obs_codes,
1149 interval_s: self.interval_s,
1150 time_of_first_obs: self.time_of_first_obs,
1151 phase_shifts: self.phase_shifts,
1152 scale_factors: self.scale_factors,
1153 glonass_slots: self.glonass_slots,
1154 marker_name: self.marker_name,
1155 };
1156 Ok(RinexObs {
1157 header,
1158 epochs: self.epochs,
1159 skipped_records: self.diagnostics.skips.len(),
1160 })
1161 }
1162
1163 fn scale_factor_for(&self, system: GnssSystem, code: &str) -> f64 {
1164 self.scale_factors
1165 .iter()
1166 .rev()
1167 .find(|record| {
1168 record.system == system
1169 && (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
1170 })
1171 .map_or(1.0, |record| record.factor)
1172 }
1173}
1174
1175fn parse_epoch_line(
1178 line: &str,
1179 second_policy: validate::CivilSecondPolicy,
1180) -> Result<(ObsEpochTime, u8, usize)> {
1181 let date_body = field(line, 1, 29);
1184 let epoch = parse_epoch_time_tokens(
1185 date_body,
1186 line,
1187 [
1188 "epoch.year",
1189 "epoch.month",
1190 "epoch.day",
1191 "epoch.hour",
1192 "epoch.minute",
1193 "epoch.second",
1194 ],
1195 second_policy,
1196 )?;
1197 let flag = strict_int_field::<u8>(line, 31, 32, "epoch.flag")?;
1198 let numsat = strict_int_field::<usize>(line, 32, 35, "epoch.satellite_count")?;
1199 Ok((epoch, flag, numsat))
1200}
1201
1202fn time_scale_from_label(label: &str, line: &str) -> Result<TimeScale> {
1206 let label = label.trim();
1207 if label.is_empty() {
1208 Ok(TimeScale::Gpst)
1209 } else {
1210 time_scale_label(label).ok_or_else(|| {
1211 Error::Parse(format!(
1212 "RINEX OBS TIME OF FIRST OBS unknown time scale {label:?} in {line:?}"
1213 ))
1214 })
1215 }
1216}
1217
1218fn civil_second_policy_for_time_scale(scale: TimeScale) -> validate::CivilSecondPolicy {
1219 match scale {
1220 TimeScale::Utc | TimeScale::Glonasst => validate::CivilSecondPolicy::UtcLike,
1222 TimeScale::Tai
1223 | TimeScale::Tt
1224 | TimeScale::Tdb
1225 | TimeScale::Gpst
1226 | TimeScale::Gst
1227 | TimeScale::Bdt
1228 | TimeScale::Qzsst => validate::CivilSecondPolicy::Continuous,
1229 }
1230}
1231
1232fn parse_epoch_time_tokens(
1233 body: &str,
1234 line: &str,
1235 fields: [&'static str; 6],
1236 second_policy: validate::CivilSecondPolicy,
1237) -> Result<ObsEpochTime> {
1238 let tokens: Vec<&str> = body.split_whitespace().collect();
1239 if tokens.len() < fields.len() {
1240 let field = fields[tokens.len()];
1241 return Err(map_field_error(FieldError::Missing { field }, line));
1242 }
1243 let year = strict_int_token::<i32>(tokens[0], fields[0], line)?;
1244 let month = strict_int_token::<i64>(tokens[1], fields[1], line)?;
1245 let day = strict_int_token::<i64>(tokens[2], fields[2], line)?;
1246 let hour = strict_int_token::<i64>(tokens[3], fields[3], line)?;
1247 let minute = strict_int_token::<i64>(tokens[4], fields[4], line)?;
1248 let second = strict_f64_token(tokens[5], fields[5], line)?;
1249 let civil = validate::civil_datetime_with_second_policy(
1250 year as i64,
1251 month,
1252 day,
1253 hour,
1254 minute,
1255 second,
1256 second_policy,
1257 )
1258 .map_err(|error| map_field_error(error, line))?;
1259 Ok(ObsEpochTime {
1260 year,
1261 month: civil.month as u8,
1262 day: civil.day as u8,
1263 hour: civil.hour as u8,
1264 minute: civil.minute as u8,
1265 second: civil.second,
1266 })
1267}
1268
1269fn strict_vec3_tokens(body: &str, line: &str, fields: [&'static str; 3]) -> Result<[f64; 3]> {
1270 let tokens: Vec<&str> = body.split_whitespace().collect();
1271 if tokens.len() < fields.len() {
1272 let field = fields[tokens.len()];
1273 return Err(map_field_error(FieldError::Missing { field }, line));
1274 }
1275 Ok([
1276 strict_f64_token(tokens[0], fields[0], line)?,
1277 strict_f64_token(tokens[1], fields[1], line)?,
1278 strict_f64_token(tokens[2], fields[2], line)?,
1279 ])
1280}
1281
1282fn strict_f64_field(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<f64> {
1283 strict_f64_token(field(line, start, end), field_name, line)
1284}
1285
1286fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
1287where
1288 T: core::str::FromStr,
1289{
1290 strict_int_token(field(line, start, end), field_name, line)
1291}
1292
1293fn strict_f64_token(token: &str, field_name: &'static str, line: &str) -> Result<f64> {
1294 validate::strict_f64(token, field_name).map_err(|error| map_field_error(error, line))
1295}
1296
1297fn validate_finite_input(value: f64, field: &'static str) -> Result<()> {
1298 if value.is_finite() {
1299 Ok(())
1300 } else {
1301 Err(Error::InvalidInput(format!(
1302 "RINEX OBS {field} must be finite"
1303 )))
1304 }
1305}
1306
1307fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
1308where
1309 T: core::str::FromStr,
1310{
1311 validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
1312}
1313
1314fn scale_factor_value(value: u32) -> Result<f64> {
1315 match value {
1316 1 | 10 | 100 | 1000 => Ok(f64::from(value)),
1317 _ => Err(Error::Parse(format!(
1318 "RINEX OBS invalid scale_factor.factor: expected 1, 10, 100, or 1000, got {value}"
1319 ))),
1320 }
1321}
1322
1323fn map_field_error(error: FieldError, line: &str) -> Error {
1324 Error::Parse(format!(
1325 "RINEX OBS invalid {}: {error} in {line:?}",
1326 error.field()
1327 ))
1328}
1329
1330fn obs_payload_field_count(payload_len: usize) -> usize {
1331 let full = payload_len / OBS_FIELD_WIDTH;
1332 let trailing = payload_len % OBS_FIELD_WIDTH;
1333 full + usize::from(trailing >= OBS_VALUE_WIDTH)
1334}
1335
1336fn sat_record_field_count(record_len: usize) -> usize {
1337 obs_payload_field_count(record_len.saturating_sub(3))
1338}
1339
1340fn ascii_fixed_columns(line: &str) -> Cow<'_, str> {
1341 if line.is_ascii() {
1342 Cow::Borrowed(line)
1343 } else {
1344 Cow::Owned(
1345 line.chars()
1346 .map(|ch| if ch.is_ascii() { ch } else { ' ' })
1347 .collect(),
1348 )
1349 }
1350}
1351
1352fn truncate_to_char_boundary(record: &mut String, len: usize) {
1353 let mut end = len.min(record.len());
1354 while !record.is_char_boundary(end) {
1355 end -= 1;
1356 }
1357 record.truncate(end);
1358}
1359
1360fn starts_with_sat_designator(line: &str) -> bool {
1368 let b = line.as_bytes();
1369 b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1].is_ascii_digit() && b[2].is_ascii_digit()
1370}
1371
1372fn consume_skipped_sat_continuations<'a, I: Iterator<Item = &'a str>>(
1376 lines: &mut std::iter::Peekable<I>,
1377) {
1378 while let Some(raw_next) = lines.peek().copied() {
1379 let next = ascii_fixed_columns(raw_next.trim_end_matches(['\r', '\n']));
1380 if next.starts_with('>') || starts_with_sat_designator(&next) {
1381 break;
1382 }
1383 lines.next();
1384 }
1385}
1386
1387fn append_sat_continuation(record: &mut String, continuation: &str, n_obs: usize) {
1388 let fields_present = sat_record_field_count(record.len());
1389 let logical_len = 3 + fields_present * OBS_FIELD_WIDTH;
1390 truncate_to_char_boundary(record, logical_len);
1391
1392 let remaining = n_obs.saturating_sub(fields_present);
1393 let payload = field(continuation, 3, continuation.len());
1394 let fields_available = obs_payload_field_count(payload.len());
1395 let fields_to_copy = remaining.min(fields_available);
1396 let width = fields_to_copy * OBS_FIELD_WIDTH;
1397 record.push_str(field(payload, 0, width));
1398}
1399
1400fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
1402 token.parse::<GnssSatelliteId>().ok()
1403}
1404
1405fn digit_at(line: &str, col: usize) -> Option<u8> {
1408 line.as_bytes()
1409 .get(col)
1410 .filter(|b| b.is_ascii_digit())
1411 .map(|b| b - b'0')
1412}
1413
1414mod write;
1415
1416#[cfg(all(test, sidereon_repo_tests))]
1417mod tests;