1use std::borrow::Cow;
39use std::collections::BTreeMap;
40
41use crate::astro::time::model::TimeScale;
42
43use crate::frequencies::{
44 rinex_band_frequency_hz, rinex_observation_frequency_hz, rinex_observation_wavelength_m,
45};
46use crate::id::{GnssSatelliteId, GnssSystem};
47use crate::parse::raw_field as field;
48use crate::rinex_nav::valid_glonass_frequency_channel;
49use crate::validate::{self, FieldError};
50use crate::{Error, Result};
51
52const OBS_FIELD_WIDTH: usize = 16;
54const OBS_VALUE_WIDTH: usize = 14;
56
57#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct ObsEpochTime {
63 pub year: i32,
65 pub month: u8,
67 pub day: u8,
69 pub hour: u8,
71 pub minute: u8,
73 pub second: f64,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct ObsValue {
81 pub value: Option<f64>,
84 pub lli: Option<u8>,
86 pub ssi: Option<u8>,
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub struct ObsPhaseShift {
93 pub system: GnssSystem,
95 pub code: String,
97 pub correction_cycles: f64,
99 pub satellites: Vec<GnssSatelliteId>,
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub struct ObsScaleFactor {
107 pub system: GnssSystem,
109 pub factor: f64,
111 pub codes: Vec<String>,
113}
114
115#[derive(Debug, Clone, PartialEq)]
118pub struct ObsEpoch {
119 pub epoch: ObsEpochTime,
121 pub flag: u8,
123 pub sats: BTreeMap<GnssSatelliteId, Vec<ObsValue>>,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct ObsHeader {
131 pub version: f64,
133 pub approx_position_m: Option<[f64; 3]>,
136 pub antenna_delta_hen_m: Option<[f64; 3]>,
140 pub obs_codes: BTreeMap<GnssSystem, Vec<String>>,
142 pub interval_s: Option<f64>,
144 pub time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
146 pub phase_shifts: Vec<ObsPhaseShift>,
148 pub scale_factors: Vec<ObsScaleFactor>,
150 pub glonass_slots: BTreeMap<u8, i8>,
152 pub marker_name: Option<String>,
154}
155
156#[derive(Debug, Clone, PartialEq)]
162pub struct RinexObs {
163 pub header: ObsHeader,
165 pub epochs: Vec<ObsEpoch>,
168}
169
170impl RinexObs {
171 pub fn parse(text: &str) -> Result<Self> {
177 let mut parser = Parser::new();
178 let mut lines = text.lines();
179 parser.parse_header(&mut lines)?;
180 parser.parse_body(&mut lines.peekable())?;
181 parser.finish()
182 }
183
184 pub fn header(&self) -> &ObsHeader {
186 &self.header
187 }
188
189 pub fn epochs(&self) -> &[ObsEpoch] {
191 &self.epochs
192 }
193
194 pub fn obs_codes(&self, sys: GnssSystem) -> Option<&[String]> {
196 self.header.obs_codes.get(&sys).map(Vec::as_slice)
197 }
198}
199
200impl core::str::FromStr for RinexObs {
201 type Err = Error;
202
203 fn from_str(s: &str) -> Result<Self> {
204 Self::parse(s)
205 }
206}
207
208#[derive(Debug, Clone, PartialEq)]
215pub struct SignalPolicy {
216 pub codes: BTreeMap<GnssSystem, Vec<String>>,
218}
219
220impl SignalPolicy {
221 pub fn default_for(version: f64) -> Result<Self> {
231 validate_finite_input(version, "version")?;
232 let mut codes = BTreeMap::new();
233 codes.insert(GnssSystem::Gps, vec!["C1C".to_string()]);
234 codes.insert(
235 GnssSystem::Galileo,
236 vec!["C1C".to_string(), "C1X".to_string()],
237 );
238 let beidou = if (3.015..3.025).contains(&version) {
243 vec!["C1I".to_string(), "C2I".to_string()]
244 } else {
245 vec!["C2I".to_string(), "C1I".to_string()]
246 };
247 codes.insert(GnssSystem::BeiDou, beidou);
248 codes.insert(GnssSystem::Glonass, vec!["C1C".to_string()]);
249 Ok(Self { codes })
250 }
251
252 pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
254 self.codes.insert(sys, codes);
255 self
256 }
257}
258
259#[derive(Debug, Clone, Default, PartialEq, Eq)]
265pub struct ObservationFilter {
266 pub codes: BTreeMap<GnssSystem, Vec<String>>,
268}
269
270impl ObservationFilter {
271 pub fn all() -> Self {
273 Self::default()
274 }
275
276 pub fn from_entries<I>(entries: I) -> Self
278 where
279 I: IntoIterator<Item = (GnssSystem, Vec<String>)>,
280 {
281 Self {
282 codes: entries.into_iter().collect(),
283 }
284 }
285
286 fn allowed_codes(&self, system: GnssSystem) -> Option<&[String]> {
287 if self.codes.is_empty() {
288 Some(&[])
289 } else {
290 self.codes.get(&system).map(Vec::as_slice)
291 }
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum ObservationKind {
298 Pseudorange,
300 CarrierPhase,
302 Doppler,
304 SignalStrength,
306 Unknown,
308}
309
310impl ObservationKind {
311 pub fn from_code(code: &str) -> Self {
313 match code.as_bytes().first().copied() {
314 Some(b'C') => Self::Pseudorange,
315 Some(b'L') => Self::CarrierPhase,
316 Some(b'D') => Self::Doppler,
317 Some(b'S') => Self::SignalStrength,
318 _ => Self::Unknown,
319 }
320 }
321
322 pub fn as_str(self) -> &'static str {
324 match self {
325 Self::Pseudorange => "pseudorange",
326 Self::CarrierPhase => "carrier_phase",
327 Self::Doppler => "doppler",
328 Self::SignalStrength => "signal_strength",
329 Self::Unknown => "unknown",
330 }
331 }
332
333 pub fn units_str(self) -> &'static str {
335 match self {
336 Self::Pseudorange => "meters",
337 Self::CarrierPhase => "cycles",
338 Self::Doppler => "hz",
339 Self::SignalStrength => "db_hz",
340 Self::Unknown => "unknown",
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq)]
347pub struct ObservationValueRow {
348 pub code: String,
350 pub kind: ObservationKind,
352 pub value: Option<f64>,
354 pub lli: Option<u8>,
356 pub ssi: Option<u8>,
358}
359
360#[derive(Debug, Clone, PartialEq)]
362pub struct CarrierPhaseRow {
363 pub code: String,
365 pub value_cycles: Option<f64>,
367 pub lli: Option<u8>,
369 pub ssi: Option<u8>,
371 pub frequency_hz: Option<f64>,
373 pub wavelength_m: Option<f64>,
375 pub value_m: Option<f64>,
377 pub phase_shift_cycles: f64,
381}
382
383pub fn observation_values(
385 obs: &RinexObs,
386 epoch: &ObsEpoch,
387 filter: &ObservationFilter,
388) -> Result<Vec<(GnssSatelliteId, Vec<ObservationValueRow>)>> {
389 let mut out = Vec::new();
390 for (sat, values) in epoch
391 .sats
392 .iter()
393 .filter(|(sat, _)| filter.allowed_codes(sat.system).is_some())
394 {
395 let allowed_codes = filter
396 .allowed_codes(sat.system)
397 .expect("filter presence checked");
398 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
399 continue;
400 };
401 let mut rows = Vec::new();
402 for (code, value) in code_list.iter().zip(values.iter()) {
403 if !allowed_codes.is_empty() && !allowed_codes.iter().any(|c| c == code) {
404 continue;
405 }
406 if let Some(value) = value.value {
407 validate_finite_input(value, "observation.value")?;
408 }
409 let kind = ObservationKind::from_code(code);
410 rows.push(ObservationValueRow {
411 code: code.clone(),
412 kind,
413 value: value.value,
414 lli: value.lli,
415 ssi: value.ssi,
416 });
417 }
418 out.push((*sat, rows));
419 }
420 Ok(out)
421}
422
423pub fn carrier_phase_rows(
425 obs: &RinexObs,
426 epoch: &ObsEpoch,
427 filter: &ObservationFilter,
428) -> Result<Vec<(GnssSatelliteId, Vec<CarrierPhaseRow>)>> {
429 validate_finite_input(obs.header.version, "version")?;
430 let mut out = Vec::new();
431 for (sat, rows) in observation_values(obs, epoch, filter)? {
432 let phases = rows
433 .into_iter()
434 .filter(|row| row.kind == ObservationKind::CarrierPhase)
435 .map(|row| carrier_phase_row(obs, sat, row))
436 .collect::<Result<Vec<_>>>()?;
437 out.push((sat, phases));
438 }
439 Ok(out)
440}
441
442pub fn band_frequency_hz(
447 system: GnssSystem,
448 band: char,
449 glonass_channel: Option<i8>,
450) -> Option<f64> {
451 rinex_band_frequency_hz(system, band, glonass_channel)
452}
453
454pub fn observation_frequency_hz(
456 system: GnssSystem,
457 code: &str,
458 rinex_version: f64,
459 glonass_channel: Option<i8>,
460) -> Result<Option<f64>> {
461 validate_finite_input(rinex_version, "version")?;
462 Ok(rinex_observation_frequency_hz(
463 system,
464 code,
465 rinex_version,
466 glonass_channel,
467 ))
468}
469
470fn carrier_phase_row(
471 obs: &RinexObs,
472 sat: GnssSatelliteId,
473 row: ObservationValueRow,
474) -> Result<CarrierPhaseRow> {
475 let glonass_channel = obs.header.glonass_slots.get(&sat.prn).copied();
476 let frequency_hz =
477 observation_frequency_hz(sat.system, &row.code, obs.header.version, glonass_channel)?;
478 let phase_shift_cycles = phase_shift_cycles(obs, sat, &row.code);
479 let value_cycles = row.value;
480 let wavelength_m =
481 rinex_observation_wavelength_m(sat.system, &row.code, obs.header.version, glonass_channel);
482 let value_m = match value_cycles.zip(wavelength_m) {
483 Some((cycles, lambda)) => {
484 let value_m = cycles * lambda;
485 validate_finite_input(value_m, "carrier_phase.value_m")?;
486 Some(value_m)
487 }
488 None => None,
489 };
490 Ok(CarrierPhaseRow {
491 code: row.code,
492 value_cycles,
493 lli: row.lli,
494 ssi: row.ssi,
495 frequency_hz,
496 wavelength_m,
497 value_m,
498 phase_shift_cycles,
499 })
500}
501
502fn phase_shift_cycles(obs: &RinexObs, sat: GnssSatelliteId, code: &str) -> f64 {
503 let mut system_wide = None;
504 for shift in obs.header.phase_shifts.iter().rev() {
505 if shift.system != sat.system || shift.code != code {
506 continue;
507 }
508 if shift.satellites.is_empty() {
509 if system_wide.is_none() {
510 system_wide = Some(shift.correction_cycles);
511 }
512 } else if shift.satellites.contains(&sat) {
513 return shift.correction_cycles;
514 }
515 }
516 system_wide.unwrap_or(0.0)
517}
518
519pub fn pseudoranges(
526 obs: &RinexObs,
527 epoch: &ObsEpoch,
528 policy: &SignalPolicy,
529) -> Result<Vec<(GnssSatelliteId, f64)>> {
530 let mut out = Vec::new();
531 for (sat, values) in &epoch.sats {
532 let Some(prefs) = policy.codes.get(&sat.system) else {
533 continue;
534 };
535 let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
536 continue;
537 };
538 for code in prefs {
539 if let Some(idx) = code_list.iter().position(|c| c == code) {
540 if let Some(ObsValue {
541 value: Some(range_m),
542 ..
543 }) = values.get(idx)
544 {
545 validate_finite_input(*range_m, "pseudorange_m")?;
546 out.push((*sat, *range_m));
547 break;
548 }
549 }
550 }
551 }
552 Ok(out)
553}
554
555struct Parser {
557 version: Option<f64>,
558 is_observation: bool,
559 approx_position_m: Option<[f64; 3]>,
560 antenna_delta_hen_m: Option<[f64; 3]>,
561 obs_codes: BTreeMap<GnssSystem, Vec<String>>,
562 interval_s: Option<f64>,
563 time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
564 phase_shifts: Vec<ObsPhaseShift>,
565 scale_factors: Vec<ObsScaleFactor>,
566 scale_factor_continuation: Option<ScaleFactorContinuation>,
567 glonass_slots: BTreeMap<u8, i8>,
568 glonass_slots_remaining: Option<usize>,
569 marker_name: Option<String>,
570 epochs: Vec<ObsEpoch>,
571 current_obs_sys: Option<GnssSystem>,
574 obs_codes_remaining: usize,
576}
577
578#[derive(Debug, Clone, Copy)]
579struct ScaleFactorContinuation {
580 remaining: usize,
581}
582
583impl Parser {
584 fn new() -> Self {
585 Self {
586 version: None,
587 is_observation: false,
588 approx_position_m: None,
589 antenna_delta_hen_m: None,
590 obs_codes: BTreeMap::new(),
591 interval_s: None,
592 time_of_first_obs: None,
593 phase_shifts: Vec::new(),
594 scale_factors: Vec::new(),
595 scale_factor_continuation: None,
596 glonass_slots: BTreeMap::new(),
597 glonass_slots_remaining: None,
598 marker_name: None,
599 epochs: Vec::new(),
600 current_obs_sys: None,
601 obs_codes_remaining: 0,
602 }
603 }
604
605 fn parse_header<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut I) -> Result<()> {
606 let mut saw_end = false;
607 for raw in lines.by_ref() {
608 let line = raw.trim_end_matches(['\r', '\n']);
609 let label = field(line, 60, 80).trim();
610 match label {
611 "RINEX VERSION / TYPE" => self.parse_version(line)?,
612 "APPROX POSITION XYZ" => self.parse_approx_position(line)?,
613 "ANTENNA: DELTA H/E/N" => self.parse_antenna_delta(line)?,
614 "SYS / # / OBS TYPES" => self.parse_obs_types(line)?,
615 "SYS / SCALE FACTOR" => self.parse_scale_factor(line)?,
616 "SYS / PHASE SHIFT" => self.parse_phase_shift(line)?,
617 "TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
618 "INTERVAL" => {
619 self.interval_s = Some(strict_f64_field(line, 0, 10, "interval_s")?);
620 }
621 "GLONASS SLOT / FRQ #" => self.parse_glonass_slots(line)?,
622 "MARKER NAME" => {
623 let name = field(line, 0, 60).trim();
624 if !name.is_empty() {
625 self.marker_name = Some(name.to_string());
626 }
627 }
628 "END OF HEADER" => {
629 self.ensure_obs_type_count_complete(line)?;
630 self.ensure_scale_factor_count_complete(line)?;
631 saw_end = true;
632 break;
633 }
634 _ => {}
636 }
637 }
638 if !saw_end {
639 return Err(Error::Parse("RINEX OBS header has no END OF HEADER".into()));
640 }
641 Ok(())
642 }
643
644 fn parse_version(&mut self, line: &str) -> Result<()> {
645 let version = field(line, 0, 20).trim();
646 let version = strict_f64_token(version, "version", line)?;
647 let type_field = field(line, 20, 40);
649 self.is_observation =
650 type_field.trim_start().starts_with('O') || type_field.contains("OBSERVATION");
651 if !self.is_observation {
652 return Err(Error::Parse(format!(
653 "RINEX file is not observation data: {type_field:?}"
654 )));
655 }
656 if version.floor() as i64 != 3 {
657 return Err(Error::Parse(format!(
658 "RINEX OBS parser requires major version 3, got {version}"
659 )));
660 }
661 self.version = Some(version);
662 Ok(())
663 }
664
665 fn parse_approx_position(&mut self, line: &str) -> Result<()> {
666 let body = field(line, 0, 60);
667 self.approx_position_m = Some(strict_vec3_tokens(
668 body,
669 line,
670 [
671 "approx_position.x_m",
672 "approx_position.y_m",
673 "approx_position.z_m",
674 ],
675 )?);
676 Ok(())
677 }
678
679 fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
680 let body = field(line, 0, 60);
681 self.antenna_delta_hen_m = Some(strict_vec3_tokens(
682 body,
683 line,
684 [
685 "antenna_delta.height_m",
686 "antenna_delta.east_m",
687 "antenna_delta.north_m",
688 ],
689 )?);
690 Ok(())
691 }
692
693 fn parse_obs_types(&mut self, line: &str) -> Result<()> {
694 let sys_field = field(line, 0, 1).trim();
698 if !sys_field.is_empty() {
699 self.ensure_obs_type_count_complete(line)?;
700 let letter = sys_field.chars().next().unwrap();
701 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
702 Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
703 })?;
704 let count = strict_int_field::<usize>(line, 3, 6, "obs_type_count")?;
705 self.current_obs_sys = Some(system);
706 self.obs_codes_remaining = count;
707 self.obs_codes.entry(system).or_default();
708 }
709 let Some(system) = self.current_obs_sys else {
710 return Ok(());
711 };
712 let codes_section = field(line, 7, 60);
715 let list = self.obs_codes.get_mut(&system).expect("system inserted");
716 for tok in codes_section.split_whitespace() {
717 if self.obs_codes_remaining == 0 {
718 return Err(Error::Parse(format!(
719 "RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
720 )));
721 }
722 list.push(tok.to_string());
723 self.obs_codes_remaining -= 1;
724 }
725 Ok(())
726 }
727
728 fn ensure_obs_type_count_complete(&self, line: &str) -> Result<()> {
729 if self.obs_codes_remaining == 0 {
730 return Ok(());
731 }
732 let Some(system) = self.current_obs_sys else {
733 return Ok(());
734 };
735 let supplied = self.obs_codes.get(&system).map_or(0, Vec::len);
736 let declared = supplied + self.obs_codes_remaining;
737 Err(Error::Parse(format!(
738 "RINEX OBS {system} SYS / # / OBS TYPES declares {declared} codes but supplies {supplied} before {line:?}"
739 )))
740 }
741
742 fn parse_phase_shift(&mut self, line: &str) -> Result<()> {
743 let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
744 if tokens.is_empty() {
745 return Ok(());
746 }
747 if tokens.len() < 2 {
748 return Err(Error::Parse(format!(
749 "RINEX OBS phase-shift header has too few fields in {line:?}"
750 )));
751 }
752
753 let system = tokens[0]
754 .chars()
755 .next()
756 .and_then(GnssSystem::from_letter)
757 .ok_or_else(|| {
758 Error::Parse(format!(
759 "RINEX OBS phase-shift system unparsable in {line:?}"
760 ))
761 })?;
762 let code = tokens[1].to_string();
763 let correction_cycles = match tokens.get(2) {
764 Some(token) => strict_f64_token(token, "phase_shift.correction_cycles", line)?,
765 None => 0.0,
766 };
767
768 let satellites = if let Some(count_token) = tokens.get(3) {
769 let count =
770 strict_int_token::<usize>(count_token, "phase_shift.satellite_count", line)?;
771 let sat_tokens = &tokens[4..];
772 if sat_tokens.len() != count {
773 return Err(Error::Parse(format!(
774 "RINEX OBS phase-shift satellite count mismatch in {line:?}"
775 )));
776 }
777 sat_tokens
778 .iter()
779 .map(|token| {
780 parse_sv_token(token).ok_or_else(|| {
781 Error::Parse(format!(
782 "RINEX OBS phase-shift satellite token {token:?} unparsable in {line:?}"
783 ))
784 })
785 })
786 .collect::<Result<Vec<_>>>()?
787 } else {
788 Vec::new()
789 };
790
791 self.phase_shifts.push(ObsPhaseShift {
792 system,
793 code,
794 correction_cycles,
795 satellites,
796 });
797 Ok(())
798 }
799
800 fn parse_scale_factor(&mut self, line: &str) -> Result<()> {
801 let sys_field = field(line, 0, 1).trim();
802 if !sys_field.is_empty() {
803 self.ensure_scale_factor_count_complete(line)?;
804 let letter = sys_field.chars().next().unwrap();
805 let system = GnssSystem::from_letter(letter).ok_or_else(|| {
806 Error::Parse(format!("RINEX OBS unknown scale-factor system {letter:?}"))
807 })?;
808 let factor =
809 scale_factor_value(strict_int_field::<u32>(line, 2, 6, "scale_factor.factor")?)?;
810 let count_field = field(line, 8, 10).trim();
811 let count = if count_field.is_empty() {
812 0
813 } else {
814 strict_int_token::<usize>(count_field, "scale_factor.obs_type_count", line)?
815 };
816 self.scale_factors.push(ObsScaleFactor {
817 system,
818 factor,
819 codes: Vec::new(),
820 });
821 if count == 0 {
822 return Ok(());
823 }
824 self.scale_factor_continuation = Some(ScaleFactorContinuation { remaining: count });
825 }
826
827 self.collect_scale_factor_codes(line)
828 }
829
830 fn collect_scale_factor_codes(&mut self, line: &str) -> Result<()> {
831 let Some(mut continuation) = self.scale_factor_continuation else {
832 return Ok(());
833 };
834 let record = self
835 .scale_factors
836 .last_mut()
837 .expect("scale factor continuation has a record");
838 for code in field(line, 10, 60).split_whitespace() {
839 if continuation.remaining == 0 {
840 return Err(Error::Parse(format!(
841 "RINEX OBS SYS / SCALE FACTOR lists more codes than declared in {line:?}"
842 )));
843 }
844 record.codes.push(code.to_string());
845 continuation.remaining -= 1;
846 }
847 self.scale_factor_continuation = (continuation.remaining > 0).then_some(continuation);
848 Ok(())
849 }
850
851 fn ensure_scale_factor_count_complete(&self, line: &str) -> Result<()> {
852 let Some(continuation) = self.scale_factor_continuation else {
853 return Ok(());
854 };
855 let supplied = self
856 .scale_factors
857 .last()
858 .map_or(0, |record| record.codes.len());
859 let declared = supplied + continuation.remaining;
860 Err(Error::Parse(format!(
861 "RINEX OBS SYS / SCALE FACTOR declares {declared} codes but supplies {supplied} before {line:?}"
862 )))
863 }
864
865 fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
866 let body = field(line, 0, 43);
867 let scale_label = field(line, 48, 51).trim();
868 let scale = time_scale_from_label(scale_label, line)?;
869 let epoch = parse_epoch_time_tokens(
870 body,
871 line,
872 [
873 "time_of_first_obs.year",
874 "time_of_first_obs.month",
875 "time_of_first_obs.day",
876 "time_of_first_obs.hour",
877 "time_of_first_obs.minute",
878 "time_of_first_obs.second",
879 ],
880 civil_second_policy_for_time_scale(scale),
881 )?;
882 self.time_of_first_obs = Some((epoch, scale));
883 Ok(())
884 }
885
886 fn parse_glonass_slots(&mut self, line: &str) -> Result<()> {
887 let count_field = field(line, 0, 3).trim();
889 if !count_field.is_empty() {
890 let count = strict_int_token::<usize>(count_field, "glonass_slot.count", line)?;
891 self.glonass_slots_remaining = Some(count);
892 }
893 let body = field(line, 4, 60);
894 let tokens: Vec<&str> = body.split_whitespace().collect();
895 if !tokens.len().is_multiple_of(2) {
896 return Err(Error::Parse(format!(
897 "RINEX OBS GLONASS slot table has an odd token count in {line:?}"
898 )));
899 }
900 for pair in tokens.chunks_exact(2) {
901 let sat = parse_sv_token(pair[0]).ok_or_else(|| {
902 Error::Parse(format!(
903 "RINEX OBS GLONASS slot satellite token {:?} unparsable in {line:?}",
904 pair[0]
905 ))
906 })?;
907 if sat.system != GnssSystem::Glonass {
908 return Err(Error::Parse(format!(
909 "RINEX OBS GLONASS slot token {:?} is not GLONASS in {line:?}",
910 pair[0]
911 )));
912 }
913 let channel = strict_int_token::<i8>(pair[1], "glonass_slot.channel", line)?;
914 if !valid_glonass_frequency_channel(i32::from(channel)) {
915 return Err(Error::Parse(format!(
916 "RINEX OBS invalid glonass_slot.channel: {channel} out of range in {line:?}"
917 )));
918 }
919 if let Some(remaining) = self.glonass_slots_remaining.as_mut() {
920 if *remaining == 0 {
921 return Err(Error::Parse(format!(
922 "RINEX OBS GLONASS slot table has more entries than declared in {line:?}"
923 )));
924 }
925 *remaining -= 1;
926 }
927 self.glonass_slots.insert(sat.prn, channel);
928 }
929 Ok(())
930 }
931
932 fn parse_body<'a, I: Iterator<Item = &'a str>>(
933 &mut self,
934 lines: &mut std::iter::Peekable<I>,
935 ) -> Result<()> {
936 while let Some(raw) = lines.next() {
937 let line = raw.trim_end_matches(['\r', '\n']);
938 if line.is_empty() {
939 continue;
940 }
941 if !line.starts_with('>') {
942 continue;
944 }
945 let time_scale = self
946 .time_of_first_obs
947 .map(|(_, scale)| scale)
948 .unwrap_or(TimeScale::Gpst);
949 let (epoch_time, flag, numsat) =
950 parse_epoch_line(line, civil_second_policy_for_time_scale(time_scale))?;
951
952 if flag > 1 {
953 for _ in 0..numsat {
957 lines
958 .next()
959 .ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
960 }
961 self.epochs.push(ObsEpoch {
962 epoch: epoch_time,
963 flag,
964 sats: BTreeMap::new(),
965 });
966 continue;
967 }
968
969 let mut sats = BTreeMap::new();
970 for _ in 0..numsat {
971 let sat_line = lines.next().ok_or_else(|| {
972 Error::Parse("RINEX OBS epoch truncated: missing satellite line".into())
973 })?;
974 let sat_line = sat_line.trim_end_matches(['\r', '\n']);
975 let sat_record = self.collect_sat_record(sat_line, lines)?;
976 let (sat, values) = self.parse_sat_line(&sat_record)?;
977 sats.insert(sat, values);
978 }
979 self.epochs.push(ObsEpoch {
980 epoch: epoch_time,
981 flag,
982 sats,
983 });
984 }
985 Ok(())
986 }
987
988 fn collect_sat_record<'a, I: Iterator<Item = &'a str>>(
989 &self,
990 first_line: &str,
991 lines: &mut std::iter::Peekable<I>,
992 ) -> Result<String> {
993 let first_line = ascii_fixed_columns(first_line);
994 let token = field(&first_line, 0, 3);
995 let sat = parse_sv_token(token).ok_or_else(|| {
996 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
997 })?;
998 let n_obs = self.obs_count_for_sat(sat)?;
999 let mut record = first_line.into_owned();
1000
1001 while sat_record_field_count(record.len()) < n_obs {
1002 let Some(raw_next) = lines.peek().copied() else {
1003 break;
1004 };
1005 let next = raw_next.trim_end_matches(['\r', '\n']);
1006 let next = ascii_fixed_columns(next);
1007 if next.starts_with('>') || starts_with_sv_token(&next) {
1008 break;
1009 }
1010 let continuation = lines.next().expect("peeked continuation line");
1011 let continuation = ascii_fixed_columns(continuation.trim_end_matches(['\r', '\n']));
1012 append_sat_continuation(&mut record, &continuation, n_obs);
1013 }
1014
1015 Ok(record)
1016 }
1017
1018 fn obs_count_for_sat(&self, sat: GnssSatelliteId) -> Result<usize> {
1019 self.obs_codes
1020 .get(&sat.system)
1021 .map(Vec::len)
1022 .ok_or_else(|| {
1023 Error::Parse(format!(
1024 "RINEX OBS satellite {sat} uses undeclared observation system"
1025 ))
1026 })
1027 }
1028
1029 fn parse_sat_line(&self, line: &str) -> Result<(GnssSatelliteId, Vec<ObsValue>)> {
1030 let token = field(line, 0, 3);
1031 let sat = parse_sv_token(token).ok_or_else(|| {
1032 Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
1033 })?;
1034 let code_list = self.obs_codes.get(&sat.system).ok_or_else(|| {
1035 Error::Parse(format!(
1036 "RINEX OBS satellite {sat} uses undeclared observation system"
1037 ))
1038 })?;
1039 let mut values = Vec::with_capacity(code_list.len());
1040 for (i, code) in code_list.iter().enumerate() {
1041 let start = 3 + i * OBS_FIELD_WIDTH;
1042 let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
1043 let value = if value_str.is_empty() {
1044 None
1045 } else {
1046 let scale = self.scale_factor_for(sat.system, code);
1047 Some(strict_f64_token(value_str, "observation.value", line)? / scale)
1048 };
1049 let lli = digit_at(line, start + OBS_VALUE_WIDTH);
1050 let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
1051 values.push(ObsValue { value, lli, ssi });
1052 }
1053 Ok((sat, values))
1054 }
1055
1056 fn finish(self) -> Result<RinexObs> {
1057 let version = self
1058 .version
1059 .ok_or_else(|| Error::Parse("RINEX OBS missing RINEX VERSION / TYPE".into()))?;
1060 if let Some(remaining) = self.glonass_slots_remaining {
1061 if remaining != 0 {
1062 return Err(Error::Parse(format!(
1063 "RINEX OBS GLONASS slot table missing {remaining} declared entries"
1064 )));
1065 }
1066 }
1067 if self.obs_codes.is_empty() {
1068 return Err(Error::Parse(
1069 "RINEX OBS header has no SYS / # / OBS TYPES records".into(),
1070 ));
1071 }
1072 let header = ObsHeader {
1073 version,
1074 approx_position_m: self.approx_position_m,
1075 antenna_delta_hen_m: self.antenna_delta_hen_m,
1076 obs_codes: self.obs_codes,
1077 interval_s: self.interval_s,
1078 time_of_first_obs: self.time_of_first_obs,
1079 phase_shifts: self.phase_shifts,
1080 scale_factors: self.scale_factors,
1081 glonass_slots: self.glonass_slots,
1082 marker_name: self.marker_name,
1083 };
1084 Ok(RinexObs {
1085 header,
1086 epochs: self.epochs,
1087 })
1088 }
1089
1090 fn scale_factor_for(&self, system: GnssSystem, code: &str) -> f64 {
1091 self.scale_factors
1092 .iter()
1093 .rev()
1094 .find(|record| {
1095 record.system == system
1096 && (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
1097 })
1098 .map_or(1.0, |record| record.factor)
1099 }
1100}
1101
1102fn parse_epoch_line(
1105 line: &str,
1106 second_policy: validate::CivilSecondPolicy,
1107) -> Result<(ObsEpochTime, u8, usize)> {
1108 let date_body = field(line, 1, 29);
1111 let epoch = parse_epoch_time_tokens(
1112 date_body,
1113 line,
1114 [
1115 "epoch.year",
1116 "epoch.month",
1117 "epoch.day",
1118 "epoch.hour",
1119 "epoch.minute",
1120 "epoch.second",
1121 ],
1122 second_policy,
1123 )?;
1124 let flag = strict_int_field::<u8>(line, 31, 32, "epoch.flag")?;
1125 let numsat = strict_int_field::<usize>(line, 32, 35, "epoch.satellite_count")?;
1126 Ok((epoch, flag, numsat))
1127}
1128
1129fn time_scale_from_label(label: &str, line: &str) -> Result<TimeScale> {
1133 let label = label.trim();
1134 if label.is_empty() {
1135 Ok(TimeScale::Gpst)
1136 } else {
1137 crate::parse::time_scale_label(label).ok_or_else(|| {
1138 Error::Parse(format!(
1139 "RINEX OBS TIME OF FIRST OBS unknown time scale {label:?} in {line:?}"
1140 ))
1141 })
1142 }
1143}
1144
1145fn civil_second_policy_for_time_scale(scale: TimeScale) -> validate::CivilSecondPolicy {
1146 match scale {
1147 TimeScale::Utc => validate::CivilSecondPolicy::UtcLike,
1148 TimeScale::Tai
1149 | TimeScale::Tt
1150 | TimeScale::Tdb
1151 | TimeScale::Gpst
1152 | TimeScale::Gst
1153 | TimeScale::Bdt => validate::CivilSecondPolicy::Continuous,
1154 }
1155}
1156
1157fn parse_epoch_time_tokens(
1158 body: &str,
1159 line: &str,
1160 fields: [&'static str; 6],
1161 second_policy: validate::CivilSecondPolicy,
1162) -> Result<ObsEpochTime> {
1163 let tokens: Vec<&str> = body.split_whitespace().collect();
1164 if tokens.len() < fields.len() {
1165 let field = fields[tokens.len()];
1166 return Err(map_field_error(FieldError::Missing { field }, line));
1167 }
1168 let year = strict_int_token::<i32>(tokens[0], fields[0], line)?;
1169 let month = strict_int_token::<i64>(tokens[1], fields[1], line)?;
1170 let day = strict_int_token::<i64>(tokens[2], fields[2], line)?;
1171 let hour = strict_int_token::<i64>(tokens[3], fields[3], line)?;
1172 let minute = strict_int_token::<i64>(tokens[4], fields[4], line)?;
1173 let second = strict_f64_token(tokens[5], fields[5], line)?;
1174 let civil = validate::civil_datetime_with_second_policy(
1175 year as i64,
1176 month,
1177 day,
1178 hour,
1179 minute,
1180 second,
1181 second_policy,
1182 )
1183 .map_err(|error| map_field_error(error, line))?;
1184 Ok(ObsEpochTime {
1185 year,
1186 month: civil.month as u8,
1187 day: civil.day as u8,
1188 hour: civil.hour as u8,
1189 minute: civil.minute as u8,
1190 second: civil.second,
1191 })
1192}
1193
1194fn strict_vec3_tokens(body: &str, line: &str, fields: [&'static str; 3]) -> Result<[f64; 3]> {
1195 let tokens: Vec<&str> = body.split_whitespace().collect();
1196 if tokens.len() < fields.len() {
1197 let field = fields[tokens.len()];
1198 return Err(map_field_error(FieldError::Missing { field }, line));
1199 }
1200 Ok([
1201 strict_f64_token(tokens[0], fields[0], line)?,
1202 strict_f64_token(tokens[1], fields[1], line)?,
1203 strict_f64_token(tokens[2], fields[2], line)?,
1204 ])
1205}
1206
1207fn strict_f64_field(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<f64> {
1208 strict_f64_token(field(line, start, end), field_name, line)
1209}
1210
1211fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
1212where
1213 T: core::str::FromStr,
1214{
1215 strict_int_token(field(line, start, end), field_name, line)
1216}
1217
1218fn strict_f64_token(token: &str, field_name: &'static str, line: &str) -> Result<f64> {
1219 validate::strict_f64(token, field_name).map_err(|error| map_field_error(error, line))
1220}
1221
1222fn validate_finite_input(value: f64, field: &'static str) -> Result<()> {
1223 if value.is_finite() {
1224 Ok(())
1225 } else {
1226 Err(Error::InvalidInput(format!(
1227 "RINEX OBS {field} must be finite"
1228 )))
1229 }
1230}
1231
1232fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
1233where
1234 T: core::str::FromStr,
1235{
1236 validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
1237}
1238
1239fn scale_factor_value(value: u32) -> Result<f64> {
1240 match value {
1241 1 | 10 | 100 | 1000 => Ok(f64::from(value)),
1242 _ => Err(Error::Parse(format!(
1243 "RINEX OBS invalid scale_factor.factor: expected 1, 10, 100, or 1000, got {value}"
1244 ))),
1245 }
1246}
1247
1248fn map_field_error(error: FieldError, line: &str) -> Error {
1249 Error::Parse(format!(
1250 "RINEX OBS invalid {}: {error} in {line:?}",
1251 error.field()
1252 ))
1253}
1254
1255fn obs_payload_field_count(payload_len: usize) -> usize {
1256 let full = payload_len / OBS_FIELD_WIDTH;
1257 let trailing = payload_len % OBS_FIELD_WIDTH;
1258 full + usize::from(trailing >= OBS_VALUE_WIDTH)
1259}
1260
1261fn sat_record_field_count(record_len: usize) -> usize {
1262 obs_payload_field_count(record_len.saturating_sub(3))
1263}
1264
1265fn ascii_fixed_columns(line: &str) -> Cow<'_, str> {
1266 if line.is_ascii() {
1267 Cow::Borrowed(line)
1268 } else {
1269 Cow::Owned(
1270 line.chars()
1271 .map(|ch| if ch.is_ascii() { ch } else { ' ' })
1272 .collect(),
1273 )
1274 }
1275}
1276
1277fn truncate_to_char_boundary(record: &mut String, len: usize) {
1278 let mut end = len.min(record.len());
1279 while !record.is_char_boundary(end) {
1280 end -= 1;
1281 }
1282 record.truncate(end);
1283}
1284
1285fn starts_with_sv_token(line: &str) -> bool {
1286 parse_sv_token(field(line, 0, 3)).is_some()
1287}
1288
1289fn append_sat_continuation(record: &mut String, continuation: &str, n_obs: usize) {
1290 let fields_present = sat_record_field_count(record.len());
1291 let logical_len = 3 + fields_present * OBS_FIELD_WIDTH;
1292 truncate_to_char_boundary(record, logical_len);
1293
1294 let remaining = n_obs.saturating_sub(fields_present);
1295 let payload = field(continuation, 3, continuation.len());
1296 let fields_available = obs_payload_field_count(payload.len());
1297 let fields_to_copy = remaining.min(fields_available);
1298 let width = fields_to_copy * OBS_FIELD_WIDTH;
1299 record.push_str(field(payload, 0, width));
1300}
1301
1302fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
1304 token.parse::<GnssSatelliteId>().ok()
1305}
1306
1307fn digit_at(line: &str, col: usize) -> Option<u8> {
1310 line.as_bytes()
1311 .get(col)
1312 .filter(|b| b.is_ascii_digit())
1313 .map(|b| b - b'0')
1314}
1315
1316#[cfg(all(test, sidereon_repo_tests))]
1317mod tests;