use std::collections::HashMap;
use std::fmt::Write as _;
use crate::format::columns::{raw_field as field, raw_field_from as field_from};
use crate::validate::{self, FieldError};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrinexVersion {
V1,
V3,
}
const MAX_ORDER: usize = 6;
#[derive(Debug, Clone, PartialEq)]
pub struct ObsStream {
pub version: CrinexVersion,
pub header: Vec<String>,
pub epochs: Vec<EpochRecord>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EpochRecord {
Obs(ObsEpoch),
Event {
descriptor: String,
lines: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObsEpoch {
pub descriptor: String,
pub clock: Option<i64>,
pub sats: Vec<SatRecord>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SatRecord {
pub sv: String,
pub values: Vec<Option<i64>>,
pub flags: String,
}
const OBS_FIELD_WIDTH: usize = 16;
const OBS_VALUE_WIDTH: usize = 14;
pub fn decode(crinex_text: &str) -> Result<String> {
let mut out = String::with_capacity(crinex_text.len() * 4);
decode_to(crinex_text, |line| {
out.push_str(line);
out.push('\n');
})?;
Ok(out)
}
pub fn decode_to<W: FnMut(&str)>(crinex_text: &str, mut emit: W) -> Result<()> {
let mut decoder = Decoder::new();
let mut lines = crinex_text.lines();
decoder.read_crinex_header(&mut lines, &mut emit)?;
decoder.read_body(&mut lines, &mut emit)?;
Ok(())
}
pub fn encode_crinex(rinex_text: &str) -> Result<String> {
let stream = parse_rinex_obs(rinex_text)?;
Ok(encode_stream(&stream))
}
#[derive(Debug, Clone)]
struct NumDiff {
m: usize,
level: usize,
buf: [i64; MAX_ORDER],
}
impl NumDiff {
fn new(data: i64, level: usize) -> Self {
let mut buf = [0i64; MAX_ORDER];
buf[0] = data;
Self { m: 0, level, buf }
}
fn force_init(&mut self, data: i64, level: usize) {
self.m = 0;
self.level = level;
self.rotate(data);
}
fn rotate(&mut self, data: i64) {
self.buf.copy_within(0..MAX_ORDER - 1, 1);
self.buf[0] = data;
}
fn decompress(&mut self, delta: i64) -> core::result::Result<i64, validate::ArithmeticError> {
let m = if self.m < self.level {
self.m + 1
} else {
self.m
};
let b = &self.buf;
let new = match m {
1 => checked_diff_sum(delta, &[(1, b[0])])?,
2 => checked_diff_sum(delta, &[(2, b[0]), (-1, b[1])])?,
3 => checked_diff_sum(delta, &[(3, b[0]), (-3, b[1]), (1, b[2])])?,
4 => checked_diff_sum(delta, &[(4, b[0]), (-6, b[1]), (4, b[2]), (-1, b[3])])?,
5 => checked_diff_sum(
delta,
&[(5, b[0]), (-10, b[1]), (10, b[2]), (-5, b[3]), (1, b[4])],
)?,
6 => checked_diff_sum(
delta,
&[
(6, b[0]),
(-15, b[1]),
(20, b[2]),
(-15, b[3]),
(6, b[4]),
(-1, b[5]),
],
)?,
_ => checked_diff_sum(delta, &[(1, b[0])])?,
};
self.m = m;
self.rotate(new);
Ok(new)
}
}
fn checked_diff_sum(
delta: i64,
terms: &[(i64, i64)],
) -> core::result::Result<i64, validate::ArithmeticError> {
const FIELD: &str = "crinex numeric difference";
let mut sum = delta;
for &(coefficient, value) in terms {
let term = validate::checked_i64_mul(coefficient.abs(), value, FIELD)?;
sum = if coefficient >= 0 {
validate::checked_i64_add(sum, term, FIELD)?
} else {
validate::checked_i64_sub(sum, term, FIELD)?
};
}
Ok(sum)
}
#[derive(Debug, Default, Clone)]
struct TextDiff {
buffer: Vec<u8>,
}
impl TextDiff {
fn force_init(&mut self, data: &str) {
self.buffer = data.as_bytes().to_vec();
}
fn decompress(&mut self, data: &str) -> String {
let bytes = data.as_bytes();
if bytes.len() > self.buffer.len() {
self.buffer.extend_from_slice(&bytes[self.buffer.len()..]);
}
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
continue;
}
if let Some(slot) = self.buffer.get_mut(i) {
*slot = if byte == b'&' { b' ' } else { byte };
}
}
String::from_utf8_lossy(&self.buffer).into_owned()
}
}
struct Decoder {
version: CrinexVersion,
obs_count: HashMap<char, usize>,
default_system: Option<char>,
epoch_diff: TextDiff,
clock_diff: Option<NumDiff>,
obs_diff: HashMap<String, Vec<Option<NumDiff>>>,
flag_diff: HashMap<String, TextDiff>,
}
impl Decoder {
fn new() -> Self {
Self {
version: CrinexVersion::V3,
obs_count: HashMap::new(),
default_system: None,
epoch_diff: TextDiff::default(),
clock_diff: None,
obs_diff: HashMap::new(),
flag_diff: HashMap::new(),
}
}
fn read_crinex_header<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
where
I: Iterator<Item = &'a str>,
W: FnMut(&str),
{
let l1 = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX stream is empty".into()))?;
let crx_ver = field(l1, 0, 20).trim();
self.version = match crx_ver {
v if v.starts_with("1.0") || v.starts_with("1.") => CrinexVersion::V1,
v if v.starts_with("3.0") || v.starts_with("3.") => CrinexVersion::V3,
other => {
return Err(Error::Parse(format!(
"unsupported CRINEX version {other:?} (expected 1.0 or 3.0)"
)))
}
};
if !l1.contains("CRINEX VERS") {
return Err(Error::Parse(
"missing CRINEX VERS / TYPE header line".into(),
));
}
lines
.next()
.ok_or_else(|| Error::Parse("CRINEX header missing PROG / DATE line".into()))?;
let mut saw_end = false;
for raw in lines.by_ref() {
let line = raw.trim_end_matches(['\r', '\n']);
emit(line);
let label = field(line, 60, 80).trim();
self.classify_header_label(line, label)?;
if label == "END OF HEADER" {
saw_end = true;
break;
}
}
if !saw_end {
return Err(Error::Parse(
"CRINEX embedded RINEX header has no END OF HEADER".into(),
));
}
Ok(())
}
fn classify_header_label(&mut self, line: &str, label: &str) -> Result<()> {
match label {
"RINEX VERSION / TYPE" => {
let sys_field = field(line, 40, 41).trim();
if let Some(c) = sys_field.chars().next() {
if c != 'M' {
self.default_system = Some(c);
}
}
}
"# / TYPES OF OBSERV" => {
let n = strict_obs_count(line, 0, 6, "rinex2.obs_type_count")?;
if let Some(sys) = self.default_system {
self.obs_count.insert(sys, n);
}
self.obs_count.entry(' ').or_insert(n);
}
"SYS / # / OBS TYPES" => {
let sys_field = field(line, 0, 1).trim();
if let Some(c) = sys_field.chars().next() {
let n = strict_obs_count(line, 3, 6, "rinex3.obs_type_count")?;
self.obs_count.insert(c, n);
}
}
_ => {}
}
Ok(())
}
fn scan_rinex_header<'a, I>(&mut self, lines: &mut I, header: &mut Vec<String>) -> Result<()>
where
I: Iterator<Item = &'a str>,
{
let mut saw_version = false;
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();
if label == "RINEX VERSION / TYPE" {
let version = field(line, 0, 9).trim();
self.version = match version.chars().next() {
Some('2') => CrinexVersion::V1,
Some('3') => CrinexVersion::V3,
_ => {
return Err(Error::Parse(format!(
"unsupported RINEX version {version:?} (expected 2 or 3)"
)))
}
};
saw_version = true;
}
self.classify_header_label(line, label)?;
header.push(line.to_string());
if label == "END OF HEADER" {
saw_end = true;
break;
}
}
if !saw_version {
return Err(Error::Parse(
"plain RINEX header missing RINEX VERSION / TYPE".into(),
));
}
if !saw_end {
return Err(Error::Parse(
"plain RINEX observation header has no END OF HEADER".into(),
));
}
Ok(())
}
fn read_body<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
where
I: Iterator<Item = &'a str>,
W: FnMut(&str),
{
let version = self.version;
loop {
let record = match version {
CrinexVersion::V3 => self.next_epoch_v3(lines)?,
CrinexVersion::V1 => self.next_epoch_v1(lines)?,
};
let Some(record) = record else { break };
match version {
CrinexVersion::V3 => serialize_rinex_epoch_v3(&record, emit),
CrinexVersion::V1 => serialize_rinex_epoch_v1(&record, emit),
}
}
Ok(())
}
fn next_epoch_v3<'a, I>(&mut self, lines: &mut I) -> Result<Option<EpochRecord>>
where
I: Iterator<Item = &'a str>,
{
let raw = loop {
match lines.next() {
None => return Ok(None),
Some(raw) => {
let line = raw.trim_end_matches(['\r', '\n']);
if !line.is_empty() {
break line;
}
}
}
};
let descriptor = if raw.starts_with('>') {
self.epoch_diff.force_init(raw);
self.epoch_diff.decompress("")
} else {
self.epoch_diff.decompress(raw)
};
let numsat = strict_int_field::<usize>(&descriptor, 32, 35, "v3.epoch.satellite_count")?;
let flag = strict_int_field::<u8>(&descriptor, 31, 32, "v3.epoch.flag")?;
if flag > 1 {
let mut event_lines = Vec::with_capacity(numsat);
for _ in 0..numsat {
let extra = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V3 event record truncated".into()))?;
event_lines.push(extra.trim_end_matches(['\r', '\n']).to_string());
}
return Ok(Some(EpochRecord::Event {
descriptor,
lines: event_lines,
}));
}
let clock_line = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V3 epoch missing clock line".into()))?
.trim_end_matches(['\r', '\n']);
let clock = self.decode_clock_value(clock_line)?;
let sv_list = self.sv_tokens_v3(&descriptor, numsat)?;
let mut sats = Vec::with_capacity(sv_list.len());
for sv in &sv_list {
let data_line = lines.next().ok_or_else(|| {
Error::Parse("CRINEX V3 epoch truncated: missing satellite line".into())
})?;
let n_obs = self.obs_count_for(sv)?;
let (values, flags) =
self.decode_sat_values(sv, data_line.trim_end_matches(['\r', '\n']), n_obs)?;
sats.push(SatRecord {
sv: sv.clone(),
values,
flags,
});
}
Ok(Some(EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
})))
}
fn sv_tokens_v3(&self, descriptor: &str, numsat: usize) -> Result<Vec<String>> {
let list = field_from(descriptor, 41);
let bytes = list.as_bytes();
let mut out = Vec::with_capacity(numsat);
for i in 0..numsat {
out.push(fixed_sv_token(bytes, "V3", numsat, i)?.to_string());
}
Ok(out)
}
fn obs_count_for(&self, sv: &str) -> Result<usize> {
let sys = sv.chars().next().unwrap_or(' ');
let count = self
.obs_count
.get(&sys)
.or_else(|| self.obs_count.get(&' '))
.copied()
.ok_or_else(|| {
Error::Parse(format!(
"CRINEX satellite {sv:?} has no declared observation count"
))
})?;
if count == 0 {
return Err(Error::Parse(format!(
"CRINEX satellite {sv:?} has zero declared observations"
)));
}
Ok(count)
}
fn decode_sat_values(
&mut self,
sv: &str,
line: &str,
n_obs: usize,
) -> Result<(Vec<Option<i64>>, String)> {
let engines = self
.obs_diff
.entry(sv.to_string())
.or_insert_with(|| vec![None; n_obs]);
if engines.len() < n_obs {
engines.resize(n_obs, None);
}
let mut values: Vec<Option<i64>> = Vec::with_capacity(n_obs);
let mut cursor = 0usize;
let bytes = line.as_bytes();
for obs_index in 0..n_obs {
if obs_index > 0 {
if cursor < bytes.len() && bytes[cursor] == b' ' {
cursor += 1;
} else if cursor >= bytes.len() {
values.push(None);
continue;
}
}
if cursor >= bytes.len() || bytes[cursor] == b' ' {
values.push(None);
continue;
}
let tok_start = cursor;
while cursor < bytes.len() && bytes[cursor] != b' ' {
cursor += 1;
}
let token = &line[tok_start..cursor];
let recovered = self.apply_obs_token(sv, obs_index, token)?;
values.push(Some(recovered));
}
let flag_raw = if cursor < bytes.len() {
let rest = &line[cursor..];
rest.strip_prefix(' ').unwrap_or(rest)
} else {
""
};
let flags = self
.flag_diff
.entry(sv.to_string())
.or_default()
.decompress(flag_raw);
Ok((values, flags))
}
fn apply_obs_token(&mut self, sv: &str, obs_index: usize, token: &str) -> Result<i64> {
let engines = self.obs_diff.get_mut(sv).expect("engines inserted above");
let slot = &mut engines[obs_index];
if let Some((order, value)) = parse_reset(token)? {
match slot {
Some(e) => e.force_init(value, order),
None => *slot = Some(NumDiff::new(value, order)),
}
Ok(value)
} else {
let delta = token.trim().parse::<i64>().map_err(|_| {
Error::Parse(format!(
"CRINEX observation delta {token:?} is not an integer"
))
})?;
let Some(engine) = slot else {
return Err(Error::Parse(format!(
"CRINEX observation {sv}[{obs_index}] has a delta before any arc init"
)));
};
engine.decompress(delta).map_err(map_arithmetic_error)
}
}
fn decode_clock_value(&mut self, line: &str) -> Result<Option<i64>> {
let token = line.trim();
if token.is_empty() {
return Ok(None);
}
let value = if let Some((order, v)) = parse_reset(token)? {
match &mut self.clock_diff {
Some(e) => e.force_init(v, order),
None => self.clock_diff = Some(NumDiff::new(v, order)),
}
v
} else {
let delta = token.parse::<i64>().map_err(|_| {
Error::Parse(format!("CRINEX clock delta {token:?} is not an integer"))
})?;
match &mut self.clock_diff {
Some(e) => e.decompress(delta).map_err(map_arithmetic_error)?,
None => {
return Err(Error::Parse(
"CRINEX clock delta before any clock arc init".into(),
))
}
}
};
Ok(Some(value))
}
fn next_epoch_v1<'a, I>(&mut self, lines: &mut I) -> Result<Option<EpochRecord>>
where
I: Iterator<Item = &'a str>,
{
let raw = loop {
match lines.next() {
None => return Ok(None),
Some(raw) => {
let line = raw.trim_end_matches(['\r', '\n']);
if !line.is_empty() {
break line;
}
}
}
};
let descriptor = if let Some(stripped) = raw.strip_prefix('&') {
self.epoch_diff.force_init(&format!(" {stripped}"));
self.epoch_diff.decompress("")
} else {
self.epoch_diff.decompress(raw)
};
let numsat = strict_int_field::<usize>(&descriptor, 29, 32, "v1.epoch.satellite_count")?;
let flag = strict_int_field::<u8>(&descriptor, 26, 29, "v1.epoch.flag")?;
if flag > 1 {
let mut event_lines = Vec::with_capacity(numsat);
for _ in 0..numsat {
let extra = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V1 event record truncated".into()))?;
event_lines.push(extra.trim_end_matches(['\r', '\n']).to_string());
}
return Ok(Some(EpochRecord::Event {
descriptor,
lines: event_lines,
}));
}
let clock_line = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V1 epoch missing clock line".into()))?
.trim_end_matches(['\r', '\n']);
let clock = self.decode_clock_value(clock_line)?;
let sv_list = self.sv_tokens_v1(&descriptor, numsat)?;
let mut sats = Vec::with_capacity(sv_list.len());
for sv in &sv_list {
let data_line = lines.next().ok_or_else(|| {
Error::Parse("CRINEX V1 epoch truncated: missing satellite line".into())
})?;
let n_obs = self.obs_count_for(sv)?;
let (values, flags) =
self.decode_sat_values(sv, data_line.trim_end_matches(['\r', '\n']), n_obs)?;
sats.push(SatRecord {
sv: sv.clone(),
values,
flags,
});
}
Ok(Some(EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
})))
}
fn sv_tokens_v1(&self, descriptor: &str, numsat: usize) -> Result<Vec<String>> {
let list = field_from(descriptor, 32);
let bytes = list.as_bytes();
let mut out = Vec::with_capacity(numsat);
for i in 0..numsat {
let mut tok = fixed_sv_token(bytes, "V1", numsat, i)?.to_string();
if tok.starts_with(' ') {
if let Some(sys) = self.default_system {
let prn = tok.trim();
tok = format!("{sys}{prn:>2}");
}
}
out.push(tok);
}
Ok(out)
}
fn parse_rinex_epochs_v3<'a, I>(&self, lines: &mut I) -> Result<Vec<EpochRecord>>
where
I: Iterator<Item = &'a str>,
{
let mut epochs = Vec::new();
loop {
let Some(line) = next_nonblank(lines) else {
return Ok(epochs);
};
if !line.starts_with('>') {
return Err(Error::Parse(format!(
"RINEX-3 epoch line must start with '>': {line:?}"
)));
}
let flag = strict_int_field::<u8>(&line, 31, 32, "v3.epoch.flag")?;
let numsat = strict_int_field::<usize>(&line, 32, 35, "v3.epoch.satellite_count")?;
if flag > 1 {
let event_lines = read_event_lines(lines, numsat, "RINEX-3")?;
epochs.push(EpochRecord::Event {
descriptor: line,
lines: event_lines,
});
continue;
}
let clock = parse_clock_field(&line, 41, 56, 12, "v3.epoch.clock")?;
let mut sats = Vec::with_capacity(numsat);
let mut sv_tokens = Vec::with_capacity(numsat);
for _ in 0..numsat {
let raw = lines.next().ok_or_else(|| {
Error::Parse("RINEX-3 epoch truncated: missing satellite line".into())
})?;
let sat_line = raw.trim_end_matches(['\r', '\n']);
let sv = field(sat_line, 0, 3).to_string();
let n_obs = self.obs_count_for(&sv)?;
let (values, flags) = parse_sat_obs_v3(sat_line, n_obs)?;
sv_tokens.push(sv.clone());
sats.push(SatRecord { sv, values, flags });
}
let descriptor = build_descriptor_v3(&line, &sv_tokens);
epochs.push(EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
}));
}
}
fn parse_rinex_epochs_v1<'a, I>(&self, lines: &mut I) -> Result<Vec<EpochRecord>>
where
I: Iterator<Item = &'a str>,
{
let mut epochs = Vec::new();
loop {
let Some(first) = next_nonblank(lines) else {
return Ok(epochs);
};
let flag = strict_int_field::<u8>(&first, 26, 29, "v1.epoch.flag")?;
let numsat = strict_int_field::<usize>(&first, 29, 32, "v1.epoch.satellite_count")?;
if flag > 1 {
let event_lines = read_event_lines(lines, numsat, "RINEX-2")?;
epochs.push(EpochRecord::Event {
descriptor: first,
lines: event_lines,
});
continue;
}
let clock = parse_clock_field(&first, 68, 80, 9, "v1.epoch.clock")?;
let mut sv_tokens: Vec<String> = Vec::with_capacity(numsat);
collect_sv_tokens_v1(&first, numsat.min(12), &mut sv_tokens);
while sv_tokens.len() < numsat {
let raw = lines.next().ok_or_else(|| {
Error::Parse("RINEX-2 epoch SV continuation truncated".into())
})?;
let cont = raw.trim_end_matches(['\r', '\n']);
let need = (numsat - sv_tokens.len()).min(12);
collect_sv_tokens_v1(cont, need, &mut sv_tokens);
}
let sv_tokens: Vec<String> = sv_tokens
.into_iter()
.map(|tok| self.normalize_v1_sv(tok))
.collect();
let mut sats = Vec::with_capacity(numsat);
for sv in &sv_tokens {
let n_obs = self.obs_count_for(sv)?;
let row_count = n_obs.div_ceil(5);
let mut obs_lines = Vec::with_capacity(row_count);
for _ in 0..row_count {
let raw = lines.next().ok_or_else(|| {
Error::Parse("RINEX-2 epoch truncated: missing observation line".into())
})?;
obs_lines.push(raw.trim_end_matches(['\r', '\n']).to_string());
}
let (values, flags) = parse_sat_obs_v1(&obs_lines, n_obs)?;
sats.push(SatRecord {
sv: sv.clone(),
values,
flags,
});
}
let descriptor = build_descriptor_v1(&first, &sv_tokens);
epochs.push(EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
}));
}
}
fn normalize_v1_sv(&self, token: String) -> String {
if token.starts_with(' ') {
if let Some(sys) = self.default_system {
let prn = token.trim();
return format!("{sys}{prn:>2}");
}
}
token
}
}
fn parse_rinex_obs(rinex_text: &str) -> Result<ObsStream> {
let mut decoder = Decoder::new();
let mut header: Vec<String> = Vec::new();
let mut lines = rinex_text.lines();
decoder.scan_rinex_header(&mut lines, &mut header)?;
let version = decoder.version;
let epochs = match version {
CrinexVersion::V3 => decoder.parse_rinex_epochs_v3(&mut lines)?,
CrinexVersion::V1 => decoder.parse_rinex_epochs_v1(&mut lines)?,
};
Ok(ObsStream {
version,
header,
epochs,
})
}
fn next_nonblank<'a, I>(lines: &mut I) -> Option<String>
where
I: Iterator<Item = &'a str>,
{
for raw in lines.by_ref() {
let line = raw.trim_end_matches(['\r', '\n']);
if !line.is_empty() {
return Some(line.to_string());
}
}
None
}
fn read_event_lines<'a, I>(lines: &mut I, count: usize, revision: &str) -> Result<Vec<String>>
where
I: Iterator<Item = &'a str>,
{
let mut out = Vec::with_capacity(count);
for _ in 0..count {
let raw = lines
.next()
.ok_or_else(|| Error::Parse(format!("{revision} event record truncated")))?;
out.push(raw.trim_end_matches(['\r', '\n']).to_string());
}
Ok(out)
}
fn parse_clock_field(
line: &str,
start: usize,
end: usize,
decimals: usize,
field_name: &'static str,
) -> Result<Option<i64>> {
let text = field(line, start, end);
if text.trim().is_empty() {
Ok(None)
} else {
Ok(Some(parse_scaled_decimal(text, decimals, field_name)?))
}
}
fn parse_sat_obs_v3(line: &str, n_obs: usize) -> Result<(Vec<Option<i64>>, String)> {
let mut values = Vec::with_capacity(n_obs);
let mut flags = String::with_capacity(n_obs * 2);
for i in 0..n_obs {
let base = 3 + i * OBS_FIELD_WIDTH;
read_obs_field(line, base, &mut values, &mut flags)?;
}
Ok((values, flags))
}
fn parse_sat_obs_v1(obs_lines: &[String], n_obs: usize) -> Result<(Vec<Option<i64>>, String)> {
let mut values = Vec::with_capacity(n_obs);
let mut flags = String::with_capacity(n_obs * 2);
for i in 0..n_obs {
let line = obs_lines.get(i / 5).map_or("", String::as_str);
let base = (i % 5) * OBS_FIELD_WIDTH;
read_obs_field(line, base, &mut values, &mut flags)?;
}
Ok((values, flags))
}
fn read_obs_field(
line: &str,
base: usize,
values: &mut Vec<Option<i64>>,
flags: &mut String,
) -> Result<()> {
let value_text = field(line, base, base + OBS_VALUE_WIDTH);
if value_text.trim().is_empty() {
values.push(None);
flags.push(' ');
flags.push(' ');
} else {
values.push(Some(parse_scaled_decimal(value_text, 3, "observation")?));
flags.push(char_at_or_space(line, base + OBS_VALUE_WIDTH));
flags.push(char_at_or_space(line, base + OBS_VALUE_WIDTH + 1));
}
Ok(())
}
fn parse_scaled_decimal(text: &str, decimals: usize, field_name: &'static str) -> Result<i64> {
let trimmed = text.trim();
let (negative, body) = trimmed.strip_prefix('-').map_or_else(
|| (false, trimmed.strip_prefix('+').unwrap_or(trimmed)),
|rest| (true, rest),
);
let (integer_part, fraction_part) = match body.split_once('.') {
Some((integer, fraction)) => (integer, fraction),
None => (body, ""),
};
let integer_text = if integer_part.is_empty() {
"0"
} else {
integer_part
};
let mut fraction = String::with_capacity(decimals);
fraction.extend(fraction_part.chars().take(decimals));
while fraction.len() < decimals {
fraction.push('0');
}
let scale = 10i64.pow(decimals as u32);
let integer_value = parse_scaled_component(integer_text, text, field_name)?;
let fraction_value = if decimals == 0 {
0
} else {
parse_scaled_component(&fraction, text, field_name)?
};
let magnitude = validate::checked_i64_mul(integer_value, scale, field_name)
.and_then(|scaled| validate::checked_i64_add(scaled, fraction_value, field_name))
.map_err(map_arithmetic_error)?;
Ok(if negative { -magnitude } else { magnitude })
}
fn parse_scaled_component(token: &str, text: &str, field_name: &'static str) -> Result<i64> {
token
.parse::<i64>()
.map_err(|_| Error::Parse(format!("CRINEX invalid {field_name}: {text:?}")))
}
fn build_descriptor_v3(epoch_line: &str, sv_tokens: &[String]) -> String {
let mut descriptor = pad_to(field(epoch_line, 0, 35), 41);
for token in sv_tokens {
descriptor.push_str(token);
}
descriptor
}
fn build_descriptor_v1(epoch_line: &str, sv_tokens: &[String]) -> String {
let mut descriptor = pad_to(field(epoch_line, 0, 32), 32);
for token in sv_tokens {
descriptor.push_str(token);
}
descriptor
}
fn collect_sv_tokens_v1(line: &str, count: usize, out: &mut Vec<String>) {
for i in 0..count {
let start = 32 + i * 3;
out.push(field(line, start, start + 3).to_string());
}
}
fn char_at_or_space(line: &str, index: usize) -> char {
line.as_bytes().get(index).map_or(' ', |&byte| byte as char)
}
fn pad_to(text: &str, width: usize) -> String {
let mut out = text.to_string();
while out.len() < width {
out.push(' ');
}
out
}
pub fn parse_stream(crinex_text: &str) -> Result<ObsStream> {
let mut decoder = Decoder::new();
let mut lines = crinex_text.lines();
let mut header: Vec<String> = Vec::new();
decoder.read_crinex_header(&mut lines, &mut |line: &str| header.push(line.to_string()))?;
let version = decoder.version;
let mut epochs = Vec::new();
loop {
let record = match version {
CrinexVersion::V3 => decoder.next_epoch_v3(&mut lines)?,
CrinexVersion::V1 => decoder.next_epoch_v1(&mut lines)?,
};
match record {
Some(record) => epochs.push(record),
None => break,
}
}
Ok(ObsStream {
version,
header,
epochs,
})
}
pub fn encode_stream(stream: &ObsStream) -> String {
let mut out = String::new();
let version_label = match stream.version {
CrinexVersion::V3 => "3.0",
CrinexVersion::V1 => "1.0",
};
push_crinex_line(
&mut out,
&labeled_crinex(version_label, "CRINEX VERS / TYPE"),
);
push_crinex_line(
&mut out,
&labeled_crinex("sidereon", "CRINEX PROG / DATE"),
);
for header_line in &stream.header {
push_crinex_line(&mut out, header_line);
}
let mut flag_state: HashMap<String, String> = HashMap::new();
for epoch in &stream.epochs {
encode_epoch(epoch, stream.version, &mut flag_state, &mut out);
}
out
}
fn encode_epoch(
epoch: &EpochRecord,
version: CrinexVersion,
flag_state: &mut HashMap<String, String>,
out: &mut String,
) {
match epoch {
EpochRecord::Event { descriptor, lines } => {
encode_descriptor(descriptor, version, out);
for line in lines {
push_crinex_line(out, line);
}
}
EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
}) => {
encode_descriptor(descriptor, version, out);
match clock {
Some(value) => push_crinex_line(out, &format!("1&{value}")),
None => push_crinex_line(out, ""),
}
for sat in sats {
let previous = flag_state.entry(sat.sv.clone()).or_default();
let delta = text_diff_delta(previous.as_str(), &sat.flags);
previous.clone_from(&sat.flags);
push_crinex_line(out, &encode_sat_line(&sat.values, &delta));
}
}
}
}
fn encode_descriptor(descriptor: &str, version: CrinexVersion, out: &mut String) {
match version {
CrinexVersion::V3 => push_crinex_line(out, descriptor),
CrinexVersion::V1 => push_crinex_line(out, &format!("&{}", &descriptor[1..])),
}
}
fn encode_sat_line(values: &[Option<i64>], flag_delta: &str) -> String {
let mut line = String::new();
for (index, value) in values.iter().enumerate() {
if index > 0 {
line.push(' ');
}
if let Some(value) = value {
let _ = write!(line, "1&{value}");
}
}
line.push(' ');
line.push_str(flag_delta);
line
}
fn text_diff_delta(previous: &str, current: &str) -> String {
let prev = previous.as_bytes();
let curr = current.as_bytes();
let mut delta = Vec::with_capacity(curr.len());
for (index, &byte) in curr.iter().enumerate() {
let out = match prev.get(index) {
Some(&previous_byte) if byte == previous_byte => b' ',
Some(_) if byte == b' ' => b'&',
_ => byte,
};
delta.push(out);
}
String::from_utf8(delta).unwrap_or_default()
}
fn push_crinex_line(out: &mut String, line: &str) {
out.push_str(line);
out.push('\n');
}
fn labeled_crinex(body: &str, label: &str) -> String {
format!("{body:<60}{label}")
}
fn serialize_rinex_epoch_v3<W: FnMut(&str)>(record: &EpochRecord, emit: &mut W) {
match record {
EpochRecord::Event { descriptor, lines } => {
emit(trim_end(field(descriptor, 0, 35)));
for line in lines {
emit(line);
}
}
EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
}) => {
let clock_text = format_clock_v3(*clock);
let head = field(descriptor, 0, 35);
let mut epoch_out = head.to_string();
if !clock_text.is_empty() {
while epoch_out.len() < 41 {
epoch_out.push(' ');
}
}
epoch_out.push_str(&clock_text);
emit(trim_end(&epoch_out));
for sat in sats {
let out = format_sat_line(&sat.sv, &sat.values, &sat.flags);
emit(trim_end(&out));
}
}
}
}
fn serialize_rinex_epoch_v1<W: FnMut(&str)>(record: &EpochRecord, emit: &mut W) {
match record {
EpochRecord::Event { descriptor, lines } => {
emit(trim_end(field(descriptor, 0, 32)));
for line in lines {
emit(line);
}
}
EpochRecord::Obs(ObsEpoch {
descriptor,
clock,
sats,
}) => {
let clock_text = format_clock_v1(*clock);
let sv_list: Vec<String> = sats.iter().map(|sat| sat.sv.clone()).collect();
for line in &format_epoch_v1(descriptor, &sv_list, &clock_text) {
emit(trim_end(line));
}
for sat in sats {
for line in format_sat_lines_v1(&sat.values, &sat.flags) {
emit(trim_end(&line));
}
}
}
}
}
fn format_clock_v3(clock: Option<i64>) -> String {
match clock {
Some(value) => format!("{:15.12}", value as f64 / 1.0e12),
None => String::new(),
}
}
fn format_clock_v1(clock: Option<i64>) -> String {
match clock {
Some(value) => format!("{:12.9}", value as f64 / 1.0e9),
None => String::new(),
}
}
fn strict_obs_count(
line: &str,
start: usize,
end: usize,
field_name: &'static str,
) -> Result<usize> {
let count = strict_int_field::<usize>(line, start, end, field_name)?;
if count == 0 {
return Err(Error::Parse(format!(
"CRINEX invalid {field_name}: observation count must be positive in {line:?}"
)));
}
Ok(count)
}
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_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 fixed_sv_token<'a>(
sv_list: &'a [u8],
crinex_version: &str,
numsat: usize,
index: usize,
) -> Result<&'a str> {
let start = index * 3;
let end = start + 3;
if end > sv_list.len() {
return Err(Error::Parse(format!(
"CRINEX {crinex_version} epoch SV list shorter than {numsat} satellites"
)));
}
let token = &sv_list[start..end];
if !token.is_ascii() {
return Err(Error::Parse(format!(
"CRINEX {crinex_version} epoch SV token {} contains non-ASCII bytes",
index + 1
)));
}
std::str::from_utf8(token).map_err(|_| {
Error::Parse(format!(
"CRINEX {crinex_version} epoch SV token {} is not valid UTF-8",
index + 1
))
})
}
fn map_field_error(error: FieldError, line: &str) -> Error {
Error::Parse(format!(
"CRINEX invalid {}: {error} in {line:?}",
error.field()
))
}
fn map_arithmetic_error(error: validate::ArithmeticError) -> Error {
Error::Parse(format!("CRINEX {error}"))
}
fn parse_reset(token: &str) -> Result<Option<(usize, i64)>> {
let token = token.trim();
if let Some(amp) = token.find('&') {
let order = token[..amp]
.parse::<usize>()
.map_err(|_| Error::Parse(format!("CRINEX reset order in {token:?} invalid")))?;
if order == 0 || order > MAX_ORDER {
return Err(Error::Parse(format!(
"CRINEX reset order {order} out of range 1..={MAX_ORDER}"
)));
}
let value = token[amp + 1..]
.parse::<i64>()
.map_err(|_| Error::Parse(format!("CRINEX reset value in {token:?} invalid")))?;
Ok(Some((order, value)))
} else {
Ok(None)
}
}
fn format_sat_line(sv: &str, values: &[Option<i64>], flags: &str) -> String {
let mut out = String::with_capacity(3 + values.len() * OBS_FIELD_WIDTH);
out.push_str(sv);
let flag_bytes = flags.as_bytes();
for (i, value) in values.iter().enumerate() {
match value {
Some(v) => out.push_str(&format_value(*v)),
None => {
for _ in 0..OBS_VALUE_WIDTH {
out.push(' ');
}
}
}
let lli = flag_bytes.get(i * 2).copied().unwrap_or(b' ');
let ssi = flag_bytes.get(i * 2 + 1).copied().unwrap_or(b' ');
if value.is_some() {
out.push(lli as char);
out.push(ssi as char);
} else {
out.push(' ');
out.push(' ');
}
}
out
}
fn format_value(scaled: i64) -> String {
let negative = scaled < 0;
let magnitude = scaled.unsigned_abs();
let whole = magnitude / 1000;
let frac = magnitude % 1000;
let body = if negative && whole == 0 {
format!("-.{frac:03}")
} else {
format!("{}{}.{:03}", if negative { "-" } else { "" }, whole, frac)
};
format!("{body:>14}")
}
fn format_epoch_v1(descriptor: &str, sv_list: &[String], clock_text: &str) -> Vec<String> {
let head = field(descriptor, 0, 32).to_string();
let mut lines = Vec::new();
let mut first = head;
for sv in sv_list.iter().take(12) {
first.push_str(sv);
}
if !clock_text.is_empty() {
while first.len() < 68 {
first.push(' ');
}
first.push_str(clock_text);
}
lines.push(first);
let mut idx = 12;
while idx < sv_list.len() {
let chunk = sv_list[idx..(idx + 12).min(sv_list.len())].join("");
lines.push(format!("{:32}{chunk}", ""));
idx += 12;
}
lines
}
fn format_sat_lines_v1(values: &[Option<i64>], flags: &str) -> Vec<String> {
let flag_bytes = flags.as_bytes();
let mut lines = Vec::new();
let mut line = String::new();
for (i, value) in values.iter().enumerate() {
if i > 0 && i % 5 == 0 {
lines.push(std::mem::take(&mut line));
}
match value {
Some(v) => line.push_str(&format_value(*v)),
None => {
for _ in 0..OBS_VALUE_WIDTH {
line.push(' ');
}
}
}
let lli = flag_bytes.get(i * 2).copied().unwrap_or(b' ');
let ssi = flag_bytes.get(i * 2 + 1).copied().unwrap_or(b' ');
if value.is_some() {
line.push(lli as char);
line.push(ssi as char);
} else {
line.push(' ');
line.push(' ');
}
}
lines.push(line);
lines
}
fn trim_end(line: &str) -> &str {
line.trim_end_matches(' ')
}
#[cfg(all(test, sidereon_repo_tests))]
mod tests;