use std::collections::HashMap;
use crate::parse::{raw_field as field, raw_field_from as field_from};
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) -> i64 {
if self.m < self.level {
self.m += 1;
}
let b = &self.buf;
let new = match self.m {
1 => delta + b[0],
2 => delta + 2 * b[0] - b[1],
3 => delta + 3 * b[0] - 3 * b[1] + b[2],
4 => delta + 4 * b[0] - 6 * b[1] + 4 * b[2] - b[3],
5 => delta + 5 * b[0] - 10 * b[1] + 10 * b[2] - 5 * b[3] + b[4],
6 => delta + 6 * b[0] - 15 * b[1] + 20 * b[2] - 15 * b[3] + 6 * b[4] - b[5],
_ => delta + b[0],
};
self.rotate(new);
new
}
}
#[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 = field(line, 0, 6).trim().parse::<usize>().unwrap_or(0);
if n > 0 {
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 = field(line, 3, 6).trim().parse::<usize>().unwrap_or(0);
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 = field(&descriptor, 32, 35)
.trim()
.parse::<usize>()
.map_err(|_| {
Error::Parse(format!(
"CRINEX V3 epoch has unparsable satellite count: {descriptor:?}"
))
})?;
let flag = field(&descriptor, 31, 32).trim().parse::<u8>().unwrap_or(0);
if flag > 1 {
emit(trim_end(field(&descriptor, 0, 35)));
for _ in 0..numsat {
if let Some(extra) = lines.next() {
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 {
let start = i * 3;
let end = start + 3;
if end > bytes.len() {
return Err(Error::Parse(format!(
"CRINEX V3 epoch SV list shorter than {numsat} satellites"
)));
}
out.push(list[start..end].to_string());
}
Ok(out)
}
fn obs_count_for(&self, sv: &str) -> usize {
let sys = sv.chars().next().unwrap_or(' ');
self.obs_count
.get(&sys)
.or_else(|| self.obs_count.get(&' '))
.copied()
.unwrap_or(0)
}
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) => Ok(e.decompress(delta)),
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),
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 = field(&descriptor, 29, 32)
.trim()
.parse::<usize>()
.map_err(|_| {
Error::Parse(format!(
"CRINEX V1 epoch has unparsable satellite count: {descriptor:?}"
))
})?;
let flag = field(&descriptor, 26, 29).trim().parse::<u8>().unwrap_or(0);
if flag > 1 {
emit(trim_end(field(&descriptor, 0, 32)));
for _ in 0..numsat {
if let Some(extra) = lines.next() {
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),
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 start = i * 3;
let end = start + 3;
if end > bytes.len() {
return Err(Error::Parse(format!(
"CRINEX V1 epoch SV list shorter than {numsat} satellites"
)));
}
let mut tok = list[start..end].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 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(test)]
mod tests;