use std::collections::BTreeMap;
use astrodynamics::time::model::TimeScale;
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::parse::raw_field as field;
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 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 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)?;
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) -> Self {
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()]);
Self { codes }
}
pub fn with_override(mut self, sys: GnssSystem, codes: Vec<String>) -> Self {
self.codes.insert(sys, codes);
self
}
}
pub fn pseudoranges(
obs: &RinexObs,
epoch: &ObsEpoch,
policy: &SignalPolicy,
) -> 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)
{
out.push((*sat, *range_m));
break;
}
}
}
}
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)>,
glonass_slots: BTreeMap<u8, i8>,
marker_name: Option<String>,
epochs: Vec<ObsEpoch>,
current_obs_sys: Option<GnssSystem>,
obs_codes_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,
glonass_slots: BTreeMap::new(),
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)?,
"TIME OF FIRST OBS" => self.parse_time_of_first_obs(line)?,
"INTERVAL" => {
self.interval_s = field(line, 0, 10).trim().parse::<f64>().ok();
}
"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" => {
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()
.parse::<f64>()
.map_err(|_| Error::Parse(format!("RINEX OBS version unparsable in {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);
let parts: Vec<f64> = body
.split_whitespace()
.filter_map(|t| t.parse::<f64>().ok())
.collect();
if parts.len() >= 3 {
self.approx_position_m = Some([parts[0], parts[1], parts[2]]);
}
Ok(())
}
fn parse_antenna_delta(&mut self, line: &str) -> Result<()> {
let body = field(line, 0, 60);
let parts: Vec<f64> = body
.split_whitespace()
.filter_map(|t| t.parse::<f64>().ok())
.collect();
if parts.len() >= 3 {
self.antenna_delta_hen_m = Some([parts[0], parts[1], parts[2]]);
}
Ok(())
}
fn parse_obs_types(&mut self, line: &str) -> Result<()> {
let sys_field = field(line, 0, 1).trim();
if !sys_field.is_empty() {
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 = field(line, 3, 6).trim().parse::<usize>().map_err(|_| {
Error::Parse(format!("RINEX OBS obs-type count unparsable in {line:?}"))
})?;
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 {
break;
}
list.push(tok.to_string());
self.obs_codes_remaining -= 1;
}
Ok(())
}
fn parse_time_of_first_obs(&mut self, line: &str) -> Result<()> {
let body = field(line, 0, 43);
let nums: Vec<f64> = body
.split_whitespace()
.filter_map(|t| t.parse::<f64>().ok())
.collect();
if nums.len() >= 6 {
let epoch = ObsEpochTime {
year: nums[0] as i32,
month: nums[1] as u8,
day: nums[2] as u8,
hour: nums[3] as u8,
minute: nums[4] as u8,
second: nums[5],
};
let scale_label = field(line, 48, 51).trim();
let scale = time_scale_from_label(scale_label);
self.time_of_first_obs = Some((epoch, scale));
}
Ok(())
}
fn parse_glonass_slots(&mut self, line: &str) {
let body = field(line, 4, 60);
let tokens: Vec<&str> = body.split_whitespace().collect();
let mut i = 0;
while i + 1 < tokens.len() {
let sv = tokens[i];
let ch = tokens[i + 1];
if let Some(rest) = sv.strip_prefix('R') {
if let (Ok(slot), Ok(channel)) = (rest.parse::<u8>(), ch.parse::<i8>()) {
self.glonass_slots.insert(slot, channel);
}
}
i += 2;
}
}
fn parse_body<'a, I: Iterator<Item = &'a str>>(&mut self, lines: &mut 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 (epoch_time, flag, numsat) = parse_epoch_line(line)?;
if flag > 1 {
for _ in 0..numsat {
let _ = lines.next();
}
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, values) = self.parse_sat_line(sat_line)?;
sats.insert(sat, values);
}
self.epochs.push(ObsEpoch {
epoch: epoch_time,
flag,
sats,
});
}
Ok(())
}
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 n_obs = self.obs_codes.get(&sat.system).map(Vec::len).unwrap_or(0);
let mut values = Vec::with_capacity(n_obs);
for i in 0..n_obs {
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 {
Some(value_str.parse::<f64>().map_err(|_| {
Error::Parse(format!(
"RINEX OBS observation {value_str:?} unparsable on {line:?}"
))
})?)
};
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 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,
glonass_slots: self.glonass_slots,
marker_name: self.marker_name,
};
Ok(RinexObs {
header,
epochs: self.epochs,
})
}
}
fn parse_epoch_line(line: &str) -> Result<(ObsEpochTime, u8, usize)> {
let date_body = field(line, 1, 29);
let nums: Vec<f64> = date_body
.split_whitespace()
.filter_map(|t| t.parse::<f64>().ok())
.collect();
if nums.len() < 6 {
return Err(Error::Parse(format!(
"RINEX OBS epoch line has too few date fields: {line:?}"
)));
}
let epoch = ObsEpochTime {
year: nums[0] as i32,
month: nums[1] as u8,
day: nums[2] as u8,
hour: nums[3] as u8,
minute: nums[4] as u8,
second: nums[5],
};
let flag = field(line, 31, 32).trim().parse::<u8>().unwrap_or(0);
let numsat = field(line, 32, 35).trim().parse::<usize>().map_err(|_| {
Error::Parse(format!(
"RINEX OBS epoch satellite count unparsable: {line:?}"
))
})?;
Ok((epoch, flag, numsat))
}
fn time_scale_from_label(label: &str) -> TimeScale {
match label.trim() {
"GPS" => TimeScale::Gpst,
"GAL" => TimeScale::Gst,
"BDT" => TimeScale::Bdt,
"UTC" => TimeScale::Utc,
"TAI" => TimeScale::Tai,
_ => TimeScale::Gpst,
}
}
fn parse_sv_token(token: &str) -> Option<GnssSatelliteId> {
let token = token.trim();
let first = token.chars().next()?;
let system = GnssSystem::from_letter(first)?;
let prn = token[first.len_utf8()..].trim().parse::<u8>().ok()?;
Some(GnssSatelliteId::new(system, prn))
}
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(test)]
mod tests;