use std::collections::HashMap;
use crate::parse::{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)]
enum CrinexVersion {
V1,
V3,
}
const MAX_ORDER: usize = 6;
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(())
}
#[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 current_sys: Option<char> = None;
let mut remaining_codes = 0usize;
let mut saw_end = false;
for raw in lines.by_ref() {
let line = raw.trim_end_matches(['\r', '\n']);
emit(line);
if line.len() >= 80 || line.len() >= 61 {
}
let label = field(line, 60, 80).trim();
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() {
current_sys = Some(c);
let n = strict_obs_count(line, 3, 6, "rinex3.obs_type_count")?;
self.obs_count.insert(c, n);
remaining_codes = n;
}
let _ = (current_sys, remaining_codes);
}
"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 read_body<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
where
I: Iterator<Item = &'a str>,
W: FnMut(&str),
{
match self.version {
CrinexVersion::V3 => self.read_body_v3(lines, emit),
CrinexVersion::V1 => self.read_body_v1(lines, emit),
}
}
fn read_body_v3<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
where
I: Iterator<Item = &'a str>,
W: FnMut(&str),
{
while let Some(raw) = lines.next() {
let line = raw.trim_end_matches(['\r', '\n']);
if line.is_empty() {
continue;
}
let descriptor = if line.starts_with('>') {
self.epoch_diff.force_init(line);
self.epoch_diff.decompress("")
} else {
self.epoch_diff.decompress(line)
};
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 {
emit(trim_end(field(&descriptor, 0, 35)));
for _ in 0..numsat {
let extra = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V3 event record truncated".into()))?;
emit(extra.trim_end_matches(['\r', '\n']));
}
continue;
}
let clock_line = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V3 epoch missing clock line".into()))?
.trim_end_matches(['\r', '\n']);
let clock_text = self.decode_clock(clock_line)?;
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));
let sv_list = self.sv_tokens_v3(&descriptor, numsat)?;
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 out =
self.decode_sat_line_v3(sv, data_line.trim_end_matches(['\r', '\n']), n_obs)?;
emit(trim_end(&out));
}
}
Ok(())
}
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_line_v3(&mut self, sv: &str, line: &str, n_obs: usize) -> Result<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(format_sat_line(sv, &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"
))
})?;
match slot {
Some(e) => e.decompress(delta).map_err(map_arithmetic_error),
None => Err(Error::Parse(format!(
"CRINEX observation {sv}[{obs_index}] has a delta before any arc init"
))),
}
}
}
fn decode_clock(&mut self, line: &str) -> Result<String> {
let token = line.trim();
if token.is_empty() {
return Ok(String::new());
}
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.trim().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(format!("{:15.12}", value as f64 / 1.0e12))
}
fn read_body_v1<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
where
I: Iterator<Item = &'a str>,
W: FnMut(&str),
{
while let Some(raw) = lines.next() {
let line = raw.trim_end_matches(['\r', '\n']);
if line.is_empty() {
continue;
}
let descriptor = if let Some(stripped) = line.strip_prefix('&') {
self.epoch_diff.force_init(&format!(" {stripped}"));
self.epoch_diff.decompress("")
} else {
self.epoch_diff.decompress(line)
};
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 {
emit(trim_end(field(&descriptor, 0, 32)));
for _ in 0..numsat {
let extra = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V1 event record truncated".into()))?;
emit(extra.trim_end_matches(['\r', '\n']));
}
continue;
}
let clock_line = lines
.next()
.ok_or_else(|| Error::Parse("CRINEX V1 epoch missing clock line".into()))?
.trim_end_matches(['\r', '\n']);
let clock_text = self.decode_clock_v1(clock_line)?;
let sv_list = self.sv_tokens_v1(&descriptor, numsat)?;
let epoch_lines = format_epoch_v1(&descriptor, &sv_list, &clock_text);
for l in &epoch_lines {
emit(trim_end(l));
}
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_v1(sv, data_line.trim_end_matches(['\r', '\n']), n_obs)?;
for l in format_sat_lines_v1(&values, &flags) {
emit(trim_end(&l));
}
}
}
Ok(())
}
fn decode_clock_v1(&mut self, line: &str) -> Result<String> {
let token = line.trim();
if token.is_empty() {
return Ok(String::new());
}
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 V1 clock delta {token:?} invalid")))?;
match &mut self.clock_diff {
Some(e) => e.decompress(delta).map_err(map_arithmetic_error)?,
None => {
return Err(Error::Parse(
"CRINEX V1 clock delta before any clock arc init".into(),
))
}
}
};
Ok(format!("{:12.9}", value as f64 / 1.0e9))
}
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 decode_sat_values_v1(
&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 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 first_chunk = sv_list
.iter()
.take(12)
.cloned()
.collect::<Vec<_>>()
.join("");
let mut first = format!("{head}{first_chunk}");
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;