use std::borrow::Cow;
use std::collections::BTreeMap;
use crate::astro::time::model::TimeScale;
use crate::frequencies::{
rinex_band_frequency_hz, rinex_observation_frequency_hz, rinex_observation_wavelength_m,
};
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::parse::raw_field as field;
use crate::rinex_nav::valid_glonass_frequency_channel;
use crate::validate::{self, FieldError};
use crate::{Error, Result};
const OBS_FIELD_WIDTH: usize = 16;
const OBS_VALUE_WIDTH: usize = 14;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ObsEpochTime {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ObsValue {
pub value: Option<f64>,
pub lli: Option<u8>,
pub ssi: Option<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObsPhaseShift {
pub system: GnssSystem,
pub code: String,
pub correction_cycles: f64,
pub satellites: Vec<GnssSatelliteId>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObsScaleFactor {
pub system: GnssSystem,
pub factor: f64,
pub codes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObsEpoch {
pub epoch: ObsEpochTime,
pub flag: u8,
pub sats: BTreeMap<GnssSatelliteId, Vec<ObsValue>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObsHeader {
pub version: f64,
pub approx_position_m: Option<[f64; 3]>,
pub antenna_delta_hen_m: Option<[f64; 3]>,
pub obs_codes: BTreeMap<GnssSystem, Vec<String>>,
pub interval_s: Option<f64>,
pub time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
pub phase_shifts: Vec<ObsPhaseShift>,
pub scale_factors: Vec<ObsScaleFactor>,
pub glonass_slots: BTreeMap<u8, i8>,
pub marker_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RinexObs {
pub header: ObsHeader,
pub epochs: Vec<ObsEpoch>,
}
impl RinexObs {
pub fn parse(text: &str) -> Result<Self> {
let mut parser = Parser::new();
let mut lines = text.lines();
parser.parse_header(&mut lines)?;
parser.parse_body(&mut lines.peekable())?;
parser.finish()
}
pub fn header(&self) -> &ObsHeader {
&self.header
}
pub fn epochs(&self) -> &[ObsEpoch] {
&self.epochs
}
pub fn obs_codes(&self, sys: GnssSystem) -> Option<&[String]> {
self.header.obs_codes.get(&sys).map(Vec::as_slice)
}
}
impl core::str::FromStr for RinexObs {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalPolicy {
pub codes: BTreeMap<GnssSystem, Vec<String>>,
}
impl SignalPolicy {
pub fn default_for(version: f64) -> Result<Self> {
validate_finite_input(version, "version")?;
let mut codes = BTreeMap::new();
codes.insert(GnssSystem::Gps, vec!["C1C".to_string()]);
codes.insert(
GnssSystem::Galileo,
vec!["C1C".to_string(), "C1X".to_string()],
);
let beidou = if (3.015..3.025).contains(&version) {
vec!["C1I".to_string(), "C2I".to_string()]
} else {
vec!["C2I".to_string(), "C1I".to_string()]
};
codes.insert(GnssSystem::BeiDou, beidou);
codes.insert(GnssSystem::Glonass, vec!["C1C".to_string()]);
Ok(Self { codes })
}
pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
self.codes.insert(sys, codes);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ObservationFilter {
pub codes: BTreeMap<GnssSystem, Vec<String>>,
}
impl ObservationFilter {
pub fn all() -> Self {
Self::default()
}
pub fn from_entries<I>(entries: I) -> Self
where
I: IntoIterator<Item = (GnssSystem, Vec<String>)>,
{
Self {
codes: entries.into_iter().collect(),
}
}
fn allowed_codes(&self, system: GnssSystem) -> Option<&[String]> {
if self.codes.is_empty() {
Some(&[])
} else {
self.codes.get(&system).map(Vec::as_slice)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObservationKind {
Pseudorange,
CarrierPhase,
Doppler,
SignalStrength,
Unknown,
}
impl ObservationKind {
pub fn from_code(code: &str) -> Self {
match code.as_bytes().first().copied() {
Some(b'C') => Self::Pseudorange,
Some(b'L') => Self::CarrierPhase,
Some(b'D') => Self::Doppler,
Some(b'S') => Self::SignalStrength,
_ => Self::Unknown,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Pseudorange => "pseudorange",
Self::CarrierPhase => "carrier_phase",
Self::Doppler => "doppler",
Self::SignalStrength => "signal_strength",
Self::Unknown => "unknown",
}
}
pub fn units_str(self) -> &'static str {
match self {
Self::Pseudorange => "meters",
Self::CarrierPhase => "cycles",
Self::Doppler => "hz",
Self::SignalStrength => "db_hz",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObservationValueRow {
pub code: String,
pub kind: ObservationKind,
pub value: Option<f64>,
pub lli: Option<u8>,
pub ssi: Option<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CarrierPhaseRow {
pub code: String,
pub value_cycles: Option<f64>,
pub lli: Option<u8>,
pub ssi: Option<u8>,
pub frequency_hz: Option<f64>,
pub wavelength_m: Option<f64>,
pub value_m: Option<f64>,
pub phase_shift_cycles: f64,
}
pub fn observation_values(
obs: &RinexObs,
epoch: &ObsEpoch,
filter: &ObservationFilter,
) -> Result<Vec<(GnssSatelliteId, Vec<ObservationValueRow>)>> {
let mut out = Vec::new();
for (sat, values) in epoch
.sats
.iter()
.filter(|(sat, _)| filter.allowed_codes(sat.system).is_some())
{
let allowed_codes = filter
.allowed_codes(sat.system)
.expect("filter presence checked");
let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
continue;
};
let mut rows = Vec::new();
for (code, value) in code_list.iter().zip(values.iter()) {
if !allowed_codes.is_empty() && !allowed_codes.iter().any(|c| c == code) {
continue;
}
if let Some(value) = value.value {
validate_finite_input(value, "observation.value")?;
}
let kind = ObservationKind::from_code(code);
rows.push(ObservationValueRow {
code: code.clone(),
kind,
value: value.value,
lli: value.lli,
ssi: value.ssi,
});
}
out.push((*sat, rows));
}
Ok(out)
}
pub fn carrier_phase_rows(
obs: &RinexObs,
epoch: &ObsEpoch,
filter: &ObservationFilter,
) -> Result<Vec<(GnssSatelliteId, Vec<CarrierPhaseRow>)>> {
validate_finite_input(obs.header.version, "version")?;
let mut out = Vec::new();
for (sat, rows) in observation_values(obs, epoch, filter)? {
let phases = rows
.into_iter()
.filter(|row| row.kind == ObservationKind::CarrierPhase)
.map(|row| carrier_phase_row(obs, sat, row))
.collect::<Result<Vec<_>>>()?;
out.push((sat, phases));
}
Ok(out)
}
pub fn band_frequency_hz(
system: GnssSystem,
band: char,
glonass_channel: Option<i8>,
) -> Option<f64> {
rinex_band_frequency_hz(system, band, glonass_channel)
}
pub fn observation_frequency_hz(
system: GnssSystem,
code: &str,
rinex_version: f64,
glonass_channel: Option<i8>,
) -> Result<Option<f64>> {
validate_finite_input(rinex_version, "version")?;
Ok(rinex_observation_frequency_hz(
system,
code,
rinex_version,
glonass_channel,
))
}
fn carrier_phase_row(
obs: &RinexObs,
sat: GnssSatelliteId,
row: ObservationValueRow,
) -> Result<CarrierPhaseRow> {
let glonass_channel = obs.header.glonass_slots.get(&sat.prn).copied();
let frequency_hz =
observation_frequency_hz(sat.system, &row.code, obs.header.version, glonass_channel)?;
let phase_shift_cycles = phase_shift_cycles(obs, sat, &row.code);
let value_cycles = row.value;
let wavelength_m =
rinex_observation_wavelength_m(sat.system, &row.code, obs.header.version, glonass_channel);
let value_m = match value_cycles.zip(wavelength_m) {
Some((cycles, lambda)) => {
let value_m = cycles * lambda;
validate_finite_input(value_m, "carrier_phase.value_m")?;
Some(value_m)
}
None => None,
};
Ok(CarrierPhaseRow {
code: row.code,
value_cycles,
lli: row.lli,
ssi: row.ssi,
frequency_hz,
wavelength_m,
value_m,
phase_shift_cycles,
})
}
fn phase_shift_cycles(obs: &RinexObs, sat: GnssSatelliteId, code: &str) -> f64 {
let mut system_wide = None;
for shift in obs.header.phase_shifts.iter().rev() {
if shift.system != sat.system || shift.code != code {
continue;
}
if shift.satellites.is_empty() {
if system_wide.is_none() {
system_wide = Some(shift.correction_cycles);
}
} else if shift.satellites.contains(&sat) {
return shift.correction_cycles;
}
}
system_wide.unwrap_or(0.0)
}
pub fn pseudoranges(
obs: &RinexObs,
epoch: &ObsEpoch,
policy: &SignalPolicy,
) -> Result<Vec<(GnssSatelliteId, f64)>> {
let mut out = Vec::new();
for (sat, values) in &epoch.sats {
let Some(prefs) = policy.codes.get(&sat.system) else {
continue;
};
let Some(code_list) = obs.header.obs_codes.get(&sat.system) else {
continue;
};
for code in prefs {
if let Some(idx) = code_list.iter().position(|c| c == code) {
if let Some(ObsValue {
value: Some(range_m),
..
}) = values.get(idx)
{
validate_finite_input(*range_m, "pseudorange_m")?;
out.push((*sat, *range_m));
break;
}
}
}
}
Ok(out)
}
struct Parser {
version: Option<f64>,
is_observation: bool,
approx_position_m: Option<[f64; 3]>,
antenna_delta_hen_m: Option<[f64; 3]>,
obs_codes: BTreeMap<GnssSystem, Vec<String>>,
interval_s: Option<f64>,
time_of_first_obs: Option<(ObsEpochTime, TimeScale)>,
phase_shifts: Vec<ObsPhaseShift>,
scale_factors: Vec<ObsScaleFactor>,
scale_factor_continuation: Option<ScaleFactorContinuation>,
glonass_slots: BTreeMap<u8, i8>,
glonass_slots_remaining: Option<usize>,
marker_name: Option<String>,
epochs: Vec<ObsEpoch>,
current_obs_sys: Option<GnssSystem>,
obs_codes_remaining: usize,
}
#[derive(Debug, Clone, Copy)]
struct ScaleFactorContinuation {
remaining: usize,
}
impl Parser {
fn new() -> Self {
Self {
version: None,
is_observation: false,
approx_position_m: None,
antenna_delta_hen_m: None,
obs_codes: BTreeMap::new(),
interval_s: None,
time_of_first_obs: None,
phase_shifts: Vec::new(),
scale_factors: Vec::new(),
scale_factor_continuation: None,
glonass_slots: BTreeMap::new(),
glonass_slots_remaining: None,
marker_name: None,
epochs: Vec::new(),
current_obs_sys: None,
obs_codes_remaining: 0,
}
}
fn parse_header<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut I) -> Result<()> {
let mut saw_end = false;
for raw in lines.by_ref() {
let line = raw.trim_end_matches(['\r', '\n']);
let label = field(line, 60, 80).trim();
match label {
"RINEX VERSION / TYPE" => self.parse_version(line)?,
"APPROX POSITION XYZ" => self.parse_approx_position(line)?,
"ANTENNA: DELTA H/E/N" => self.parse_antenna_delta(line)?,
"SYS / # / OBS TYPES" => self.parse_obs_types(line)?,
"SYS / SCALE FACTOR" => self.parse_scale_factor(line)?,
"SYS / PHASE SHIFT" => self.parse_phase_shift(line)?,
"TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
"INTERVAL" => {
self.interval_s = Some(strict_f64_field(line, 0, 10, "interval_s")?);
}
"GLONASS SLOT / FRQ #" => self.parse_glonass_slots(line)?,
"MARKER NAME" => {
let name = field(line, 0, 60).trim();
if !name.is_empty() {
self.marker_name = Some(name.to_string());
}
}
"END OF HEADER" => {
self.ensure_obs_type_count_complete(line)?;
self.ensure_scale_factor_count_complete(line)?;
saw_end = true;
break;
}
_ => {}
}
}
if !saw_end {
return Err(Error::Parse("RINEX OBS header has no END OF HEADER".into()));
}
Ok(())
}
fn parse_version(&mut self, line: &str) -> Result<()> {
let version = field(line, 0, 20).trim();
let version = strict_f64_token(version, "version", line)?;
let type_field = field(line, 20, 40);
self.is_observation =
type_field.trim_start().starts_with('O') || type_field.contains("OBSERVATION");
if !self.is_observation {
return Err(Error::Parse(format!(
"RINEX file is not observation data: {type_field:?}"
)));
}
if version.floor() as i64 != 3 {
return Err(Error::Parse(format!(
"RINEX OBS parser requires major version 3, got {version}"
)));
}
self.version = Some(version);
Ok(())
}
fn parse_approx_position(&mut self, line: &str) -> Result<()> {
let body = field(line, 0, 60);
self.approx_position_m = Some(strict_vec3_tokens(
body,
line,
[
"approx_position.x_m",
"approx_position.y_m",
"approx_position.z_m",
],
)?);
Ok(())
}
fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
let body = field(line, 0, 60);
self.antenna_delta_hen_m = Some(strict_vec3_tokens(
body,
line,
[
"antenna_delta.height_m",
"antenna_delta.east_m",
"antenna_delta.north_m",
],
)?);
Ok(())
}
fn parse_obs_types(&mut self, line: &str) -> Result<()> {
let sys_field = field(line, 0, 1).trim();
if !sys_field.is_empty() {
self.ensure_obs_type_count_complete(line)?;
let letter = sys_field.chars().next().unwrap();
let system = GnssSystem::from_letter(letter).ok_or_else(|| {
Error::Parse(format!("RINEX OBS unknown system letter {letter:?}"))
})?;
let count = strict_int_field::<usize>(line, 3, 6, "obs_type_count")?;
self.current_obs_sys = Some(system);
self.obs_codes_remaining = count;
self.obs_codes.entry(system).or_default();
}
let Some(system) = self.current_obs_sys else {
return Ok(());
};
let codes_section = field(line, 7, 60);
let list = self.obs_codes.get_mut(&system).expect("system inserted");
for tok in codes_section.split_whitespace() {
if self.obs_codes_remaining == 0 {
return Err(Error::Parse(format!(
"RINEX OBS {system} SYS / # / OBS TYPES lists more codes than declared in {line:?}"
)));
}
list.push(tok.to_string());
self.obs_codes_remaining -= 1;
}
Ok(())
}
fn ensure_obs_type_count_complete(&self, line: &str) -> Result<()> {
if self.obs_codes_remaining == 0 {
return Ok(());
}
let Some(system) = self.current_obs_sys else {
return Ok(());
};
let supplied = self.obs_codes.get(&system).map_or(0, Vec::len);
let declared = supplied + self.obs_codes_remaining;
Err(Error::Parse(format!(
"RINEX OBS {system} SYS / # / OBS TYPES declares {declared} codes but supplies {supplied} before {line:?}"
)))
}
fn parse_phase_shift(&mut self, line: &str) -> Result<()> {
let tokens: Vec<&str> = field(line, 0, 60).split_whitespace().collect();
if tokens.is_empty() {
return Ok(());
}
if tokens.len() < 2 {
return Err(Error::Parse(format!(
"RINEX OBS phase-shift header has too few fields in {line:?}"
)));
}
let system = tokens[0]
.chars()
.next()
.and_then(GnssSystem::from_letter)
.ok_or_else(|| {
Error::Parse(format!(
"RINEX OBS phase-shift system unparsable in {line:?}"
))
})?;
let code = tokens[1].to_string();
let correction_cycles = match tokens.get(2) {
Some(token) => strict_f64_token(token, "phase_shift.correction_cycles", line)?,
None => 0.0,
};
let satellites = if let Some(count_token) = tokens.get(3) {
let count =
strict_int_token::<usize>(count_token, "phase_shift.satellite_count", line)?;
let sat_tokens = &tokens[4..];
if sat_tokens.len() != count {
return Err(Error::Parse(format!(
"RINEX OBS phase-shift satellite count mismatch in {line:?}"
)));
}
sat_tokens
.iter()
.map(|token| {
parse_sv_token(token).ok_or_else(|| {
Error::Parse(format!(
"RINEX OBS phase-shift satellite token {token:?} unparsable in {line:?}"
))
})
})
.collect::<Result<Vec<_>>>()?
} else {
Vec::new()
};
self.phase_shifts.push(ObsPhaseShift {
system,
code,
correction_cycles,
satellites,
});
Ok(())
}
fn parse_scale_factor(&mut self, line: &str) -> Result<()> {
let sys_field = field(line, 0, 1).trim();
if !sys_field.is_empty() {
self.ensure_scale_factor_count_complete(line)?;
let letter = sys_field.chars().next().unwrap();
let system = GnssSystem::from_letter(letter).ok_or_else(|| {
Error::Parse(format!("RINEX OBS unknown scale-factor system {letter:?}"))
})?;
let factor =
scale_factor_value(strict_int_field::<u32>(line, 2, 6, "scale_factor.factor")?)?;
let count_field = field(line, 8, 10).trim();
let count = if count_field.is_empty() {
0
} else {
strict_int_token::<usize>(count_field, "scale_factor.obs_type_count", line)?
};
self.scale_factors.push(ObsScaleFactor {
system,
factor,
codes: Vec::new(),
});
if count == 0 {
return Ok(());
}
self.scale_factor_continuation = Some(ScaleFactorContinuation { remaining: count });
}
self.collect_scale_factor_codes(line)
}
fn collect_scale_factor_codes(&mut self, line: &str) -> Result<()> {
let Some(mut continuation) = self.scale_factor_continuation else {
return Ok(());
};
let record = self
.scale_factors
.last_mut()
.expect("scale factor continuation has a record");
for code in field(line, 10, 60).split_whitespace() {
if continuation.remaining == 0 {
return Err(Error::Parse(format!(
"RINEX OBS SYS / SCALE FACTOR lists more codes than declared in {line:?}"
)));
}
record.codes.push(code.to_string());
continuation.remaining -= 1;
}
self.scale_factor_continuation = (continuation.remaining > 0).then_some(continuation);
Ok(())
}
fn ensure_scale_factor_count_complete(&self, line: &str) -> Result<()> {
let Some(continuation) = self.scale_factor_continuation else {
return Ok(());
};
let supplied = self
.scale_factors
.last()
.map_or(0, |record| record.codes.len());
let declared = supplied + continuation.remaining;
Err(Error::Parse(format!(
"RINEX OBS SYS / SCALE FACTOR declares {declared} codes but supplies {supplied} before {line:?}"
)))
}
fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
let body = field(line, 0, 43);
let scale_label = field(line, 48, 51).trim();
let scale = time_scale_from_label(scale_label, line)?;
let epoch = parse_epoch_time_tokens(
body,
line,
[
"time_of_first_obs.year",
"time_of_first_obs.month",
"time_of_first_obs.day",
"time_of_first_obs.hour",
"time_of_first_obs.minute",
"time_of_first_obs.second",
],
civil_second_policy_for_time_scale(scale),
)?;
self.time_of_first_obs = Some((epoch, scale));
Ok(())
}
fn parse_glonass_slots(&mut self, line: &str) -> Result<()> {
let count_field = field(line, 0, 3).trim();
if !count_field.is_empty() {
let count = strict_int_token::<usize>(count_field, "glonass_slot.count", line)?;
self.glonass_slots_remaining = Some(count);
}
let body = field(line, 4, 60);
let tokens: Vec<&str> = body.split_whitespace().collect();
if !tokens.len().is_multiple_of(2) {
return Err(Error::Parse(format!(
"RINEX OBS GLONASS slot table has an odd token count in {line:?}"
)));
}
for pair in tokens.chunks_exact(2) {
let sat = parse_sv_token(pair[0]).ok_or_else(|| {
Error::Parse(format!(
"RINEX OBS GLONASS slot satellite token {:?} unparsable in {line:?}",
pair[0]
))
})?;
if sat.system != GnssSystem::Glonass {
return Err(Error::Parse(format!(
"RINEX OBS GLONASS slot token {:?} is not GLONASS in {line:?}",
pair[0]
)));
}
let channel = strict_int_token::<i8>(pair[1], "glonass_slot.channel", line)?;
if !valid_glonass_frequency_channel(i32::from(channel)) {
return Err(Error::Parse(format!(
"RINEX OBS invalid glonass_slot.channel: {channel} out of range in {line:?}"
)));
}
if let Some(remaining) = self.glonass_slots_remaining.as_mut() {
if *remaining == 0 {
return Err(Error::Parse(format!(
"RINEX OBS GLONASS slot table has more entries than declared in {line:?}"
)));
}
*remaining -= 1;
}
self.glonass_slots.insert(sat.prn, channel);
}
Ok(())
}
fn parse_body<'a, I: Iterator<Item = &'a str>>(
&mut self,
lines: &mut std::iter::Peekable<I>,
) -> Result<()> {
while let Some(raw) = lines.next() {
let line = raw.trim_end_matches(['\r', '\n']);
if line.is_empty() {
continue;
}
if !line.starts_with('>') {
continue;
}
let time_scale = self
.time_of_first_obs
.map(|(_, scale)| scale)
.unwrap_or(TimeScale::Gpst);
let (epoch_time, flag, numsat) =
parse_epoch_line(line, civil_second_policy_for_time_scale(time_scale))?;
if flag > 1 {
for _ in 0..numsat {
lines
.next()
.ok_or_else(|| Error::Parse("RINEX OBS event record truncated".into()))?;
}
self.epochs.push(ObsEpoch {
epoch: epoch_time,
flag,
sats: BTreeMap::new(),
});
continue;
}
let mut sats = BTreeMap::new();
for _ in 0..numsat {
let sat_line = lines.next().ok_or_else(|| {
Error::Parse("RINEX OBS epoch truncated: missing satellite line".into())
})?;
let sat_line = sat_line.trim_end_matches(['\r', '\n']);
let sat_record = self.collect_sat_record(sat_line, lines)?;
let (sat, values) = self.parse_sat_line(&sat_record)?;
sats.insert(sat, values);
}
self.epochs.push(ObsEpoch {
epoch: epoch_time,
flag,
sats,
});
}
Ok(())
}
fn collect_sat_record<'a, I: Iterator<Item = &'a str>>(
&self,
first_line: &str,
lines: &mut std::iter::Peekable<I>,
) -> Result<String> {
let first_line = ascii_fixed_columns(first_line);
let token = field(&first_line, 0, 3);
let sat = parse_sv_token(token).ok_or_else(|| {
Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
})?;
let n_obs = self.obs_count_for_sat(sat)?;
let mut record = first_line.into_owned();
while sat_record_field_count(record.len()) < n_obs {
let Some(raw_next) = lines.peek().copied() else {
break;
};
let next = raw_next.trim_end_matches(['\r', '\n']);
let next = ascii_fixed_columns(next);
if next.starts_with('>') || starts_with_sv_token(&next) {
break;
}
let continuation = lines.next().expect("peeked continuation line");
let continuation = ascii_fixed_columns(continuation.trim_end_matches(['\r', '\n']));
append_sat_continuation(&mut record, &continuation, n_obs);
}
Ok(record)
}
fn obs_count_for_sat(&self, sat: GnssSatelliteId) -> Result<usize> {
self.obs_codes
.get(&sat.system)
.map(Vec::len)
.ok_or_else(|| {
Error::Parse(format!(
"RINEX OBS satellite {sat} uses undeclared observation system"
))
})
}
fn parse_sat_line(&self, line: &str) -> Result<(GnssSatelliteId, Vec<ObsValue>)> {
let token = field(line, 0, 3);
let sat = parse_sv_token(token).ok_or_else(|| {
Error::Parse(format!("RINEX OBS unparsable satellite token {token:?}"))
})?;
let code_list = self.obs_codes.get(&sat.system).ok_or_else(|| {
Error::Parse(format!(
"RINEX OBS satellite {sat} uses undeclared observation system"
))
})?;
let mut values = Vec::with_capacity(code_list.len());
for (i, code) in code_list.iter().enumerate() {
let start = 3 + i * OBS_FIELD_WIDTH;
let value_str = field(line, start, start + OBS_VALUE_WIDTH).trim();
let value = if value_str.is_empty() {
None
} else {
let scale = self.scale_factor_for(sat.system, code);
Some(strict_f64_token(value_str, "observation.value", line)? / scale)
};
let lli = digit_at(line, start + OBS_VALUE_WIDTH);
let ssi = digit_at(line, start + OBS_VALUE_WIDTH + 1);
values.push(ObsValue { value, lli, ssi });
}
Ok((sat, values))
}
fn finish(self) -> Result<RinexObs> {
let version = self
.version
.ok_or_else(|| Error::Parse("RINEX OBS missing RINEX VERSION / TYPE".into()))?;
if let Some(remaining) = self.glonass_slots_remaining {
if remaining != 0 {
return Err(Error::Parse(format!(
"RINEX OBS GLONASS slot table missing {remaining} declared entries"
)));
}
}
if self.obs_codes.is_empty() {
return Err(Error::Parse(
"RINEX OBS header has no SYS / # / OBS TYPES records".into(),
));
}
let header = ObsHeader {
version,
approx_position_m: self.approx_position_m,
antenna_delta_hen_m: self.antenna_delta_hen_m,
obs_codes: self.obs_codes,
interval_s: self.interval_s,
time_of_first_obs: self.time_of_first_obs,
phase_shifts: self.phase_shifts,
scale_factors: self.scale_factors,
glonass_slots: self.glonass_slots,
marker_name: self.marker_name,
};
Ok(RinexObs {
header,
epochs: self.epochs,
})
}
fn scale_factor_for(&self, system: GnssSystem, code: &str) -> f64 {
self.scale_factors
.iter()
.rev()
.find(|record| {
record.system == system
&& (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
})
.map_or(1.0, |record| record.factor)
}
}
fn parse_epoch_line(
line: &str,
second_policy: validate::CivilSecondPolicy,
) -> Result<(ObsEpochTime, u8, usize)> {
let date_body = field(line, 1, 29);
let epoch = parse_epoch_time_tokens(
date_body,
line,
[
"epoch.year",
"epoch.month",
"epoch.day",
"epoch.hour",
"epoch.minute",
"epoch.second",
],
second_policy,
)?;
let flag = strict_int_field::<u8>(line, 31, 32, "epoch.flag")?;
let numsat = strict_int_field::<usize>(line, 32, 35, "epoch.satellite_count")?;
Ok((epoch, flag, numsat))
}
fn time_scale_from_label(label: &str, line: &str) -> Result<TimeScale> {
let label = label.trim();
if label.is_empty() {
Ok(TimeScale::Gpst)
} else {
crate::parse::time_scale_label(label).ok_or_else(|| {
Error::Parse(format!(
"RINEX OBS TIME OF FIRST OBS unknown time scale {label:?} in {line:?}"
))
})
}
}
fn civil_second_policy_for_time_scale(scale: TimeScale) -> validate::CivilSecondPolicy {
match scale {
TimeScale::Utc => validate::CivilSecondPolicy::UtcLike,
TimeScale::Tai
| TimeScale::Tt
| TimeScale::Tdb
| TimeScale::Gpst
| TimeScale::Gst
| TimeScale::Bdt => validate::CivilSecondPolicy::Continuous,
}
}
fn parse_epoch_time_tokens(
body: &str,
line: &str,
fields: [&'static str; 6],
second_policy: validate::CivilSecondPolicy,
) -> Result<ObsEpochTime> {
let tokens: Vec<&str> = body.split_whitespace().collect();
if tokens.len() < fields.len() {
let field = fields[tokens.len()];
return Err(map_field_error(FieldError::Missing { field }, line));
}
let year = strict_int_token::<i32>(tokens[0], fields[0], line)?;
let month = strict_int_token::<i64>(tokens[1], fields[1], line)?;
let day = strict_int_token::<i64>(tokens[2], fields[2], line)?;
let hour = strict_int_token::<i64>(tokens[3], fields[3], line)?;
let minute = strict_int_token::<i64>(tokens[4], fields[4], line)?;
let second = strict_f64_token(tokens[5], fields[5], line)?;
let civil = validate::civil_datetime_with_second_policy(
year as i64,
month,
day,
hour,
minute,
second,
second_policy,
)
.map_err(|error| map_field_error(error, line))?;
Ok(ObsEpochTime {
year,
month: civil.month as u8,
day: civil.day as u8,
hour: civil.hour as u8,
minute: civil.minute as u8,
second: civil.second,
})
}
fn strict_vec3_tokens(body: &str, line: &str, fields: [&'static str; 3]) -> Result<[f64; 3]> {
let tokens: Vec<&str> = body.split_whitespace().collect();
if tokens.len() < fields.len() {
let field = fields[tokens.len()];
return Err(map_field_error(FieldError::Missing { field }, line));
}
Ok([
strict_f64_token(tokens[0], fields[0], line)?,
strict_f64_token(tokens[1], fields[1], line)?,
strict_f64_token(tokens[2], fields[2], line)?,
])
}
fn strict_f64_field(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<f64> {
strict_f64_token(field(line, start, end), field_name, line)
}
fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
where
T: core::str::FromStr,
{
strict_int_token(field(line, start, end), field_name, line)
}
fn strict_f64_token(token: &str, field_name: &'static str, line: &str) -> Result<f64> {
validate::strict_f64(token, field_name).map_err(|error| map_field_error(error, line))
}
fn validate_finite_input(value: f64, field: &'static str) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
Err(Error::InvalidInput(format!(
"RINEX OBS {field} must be finite"
)))
}
}
fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
where
T: core::str::FromStr,
{
validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
}
fn scale_factor_value(value: u32) -> Result<f64> {
match value {
1 | 10 | 100 | 1000 => Ok(f64::from(value)),
_ => Err(Error::Parse(format!(
"RINEX OBS invalid scale_factor.factor: expected 1, 10, 100, or 1000, got {value}"
))),
}
}
fn map_field_error(error: FieldError, line: &str) -> Error {
Error::Parse(format!(
"RINEX OBS invalid {}: {error} in {line:?}",
error.field()
))
}
fn obs_payload_field_count(payload_len: usize) -> usize {
let full = payload_len / OBS_FIELD_WIDTH;
let trailing = payload_len % OBS_FIELD_WIDTH;
full + usize::from(trailing >= OBS_VALUE_WIDTH)
}
fn sat_record_field_count(record_len: usize) -> usize {
obs_payload_field_count(record_len.saturating_sub(3))
}
fn ascii_fixed_columns(line: &str) -> Cow<'_, str> {
if line.is_ascii() {
Cow::Borrowed(line)
} else {
Cow::Owned(
line.chars()
.map(|ch| if ch.is_ascii() { ch } else { ' ' })
.collect(),
)
}
}
fn truncate_to_char_boundary(record: &mut String, len: usize) {
let mut end = len.min(record.len());
while !record.is_char_boundary(end) {
end -= 1;
}
record.truncate(end);
}
fn starts_with_sv_token(line: &str) -> bool {
parse_sv_token(field(line, 0, 3)).is_some()
}
fn append_sat_continuation(record: &mut String, continuation: &str, n_obs: usize) {
let fields_present = sat_record_field_count(record.len());
let logical_len = 3 + fields_present * OBS_FIELD_WIDTH;
truncate_to_char_boundary(record, logical_len);
let remaining = n_obs.saturating_sub(fields_present);
let payload = field(continuation, 3, continuation.len());
let fields_available = obs_payload_field_count(payload.len());
let fields_to_copy = remaining.min(fields_available);
let width = fields_to_copy * OBS_FIELD_WIDTH;
record.push_str(field(payload, 0, width));
}
fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
token.parse::<GnssSatelliteId>().ok()
}
fn digit_at(line: &str, col: usize) -> Option<u8> {
line.as_bytes()
.get(col)
.filter(|b| b.is_ascii_digit())
.map(|b| b - b'0')
}
#[cfg(all(test, sidereon_repo_tests))]
mod tests;