use super::{FileLocation, FormatError, ParseMode};
use affn::cartesian;
use affn::centers::{AffineCenter, ReferenceCenter};
use affn::frames::ITRF;
use qtty::unit::Meter;
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read};
#[derive(Debug, Copy, Clone)]
pub struct GeoCenterItrf;
impl ReferenceCenter for GeoCenterItrf {
type Params = ();
fn center_name() -> &'static str {
"Geocentric ITRF"
}
}
impl AffineCenter for GeoCenterItrf {}
pub type SinexPosition = cartesian::Position<GeoCenterItrf, ITRF, Meter>;
pub type MeterPerYear = qtty::Per<qtty::unit::Meter, qtty::unit::Year>;
#[derive(Debug, Clone)]
pub struct StationCoordinate {
pub code: String,
pub point_code: String,
pub solution_id: String,
pub ref_epoch_mjd: f64,
pub position: SinexPosition,
pub velocity_m_yr: Option<[f64; 3]>,
}
#[derive(Debug, Default)]
pub struct SinexSolution {
pub agency: String,
pub description: String,
pub stations: Vec<StationCoordinate>,
}
pub fn read_sinex<R: Read>(reader: R, mode: ParseMode) -> Result<SinexSolution, FormatError> {
let mut solution = SinexSolution::default();
let buf = BufReader::new(reader);
#[derive(Default)]
struct EstRow {
xyz: [Option<f64>; 3],
vel: [Option<f64>; 3],
ref_epoch_mjd: f64,
pt: String,
soln: String,
}
let mut estimates: HashMap<String, EstRow> = HashMap::new();
#[derive(PartialEq)]
enum Block {
None,
SiteId,
Estimate,
}
let mut block = Block::None;
for (lineno, result) in buf.lines().enumerate() {
let line = result.map_err(FormatError::Io)?;
let lineno = lineno + 1;
if line.starts_with("%=SNX") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
solution.agency = parts[2].to_string();
}
continue;
}
if line.starts_with("%ENDSNX") {
break;
}
if line.starts_with('*') || line.starts_with('%') {
continue;
}
if let Some(bname) = line.strip_prefix('+') {
match bname.trim() {
"SITE/ID" => block = Block::SiteId,
"SOLUTION/ESTIMATE" => block = Block::Estimate,
_ => block = Block::None,
}
continue;
}
if line.starts_with('-') {
block = Block::None;
continue;
}
match block {
Block::SiteId => {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let _ = parts[0];
}
Block::Estimate => {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 9 {
if mode == ParseMode::Strict {
return Err(FormatError::located(
"SINEX 2.10 §4.2",
FileLocation::at_line(lineno),
format!("ESTIMATE line has {} fields, need ≥ 9", parts.len()),
));
}
continue;
}
let est_type = parts[1];
let code = parts[2].to_string();
let pt = parts[3].to_string();
let soln = parts[4].to_string();
let epoch_raw = parts[5];
let value: f64 = match parts[8].parse() {
Ok(v) => v,
Err(_) => {
if mode == ParseMode::Strict {
return Err(FormatError::located(
"SINEX 2.10 §4.2",
FileLocation::at_line(lineno),
format!("cannot parse value: {}", parts[8]),
));
}
continue;
}
};
let ref_epoch_mjd = sinex_epoch_to_mjd(epoch_raw);
let row = estimates.entry(code.clone()).or_default();
row.pt = pt;
row.soln = soln;
row.ref_epoch_mjd = ref_epoch_mjd;
match est_type {
"STAX" => row.xyz[0] = Some(value),
"STAY" => row.xyz[1] = Some(value),
"STAZ" => row.xyz[2] = Some(value),
"VELX" => row.vel[0] = Some(value),
"VELY" => row.vel[1] = Some(value),
"VELZ" => row.vel[2] = Some(value),
_ => {}
}
}
Block::None => {}
}
}
for (code, row) in estimates {
let (x, y, z) = match (row.xyz[0], row.xyz[1], row.xyz[2]) {
(Some(x), Some(y), Some(z)) => (x, y, z),
_ => continue,
};
let velocity_m_yr = match (row.vel[0], row.vel[1], row.vel[2]) {
(Some(vx), Some(vy), Some(vz)) => Some([vx, vy, vz]),
_ => None,
};
solution.stations.push(StationCoordinate {
code,
point_code: row.pt,
solution_id: row.soln,
ref_epoch_mjd: row.ref_epoch_mjd,
position: SinexPosition::new(x, y, z),
velocity_m_yr,
});
}
Ok(solution)
}
fn sinex_epoch_to_mjd(s: &str) -> f64 {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 {
return 0.0;
}
let yy: i32 = parts[0].parse().unwrap_or(0);
let doy: u32 = parts[1].parse().unwrap_or(1);
let sod: f64 = parts[2].parse().unwrap_or(0.0);
let yyyy = if yy == 0 {
2000
} else if yy >= 80 {
1900 + yy
} else {
2000 + yy
};
let epoch = match chrono::NaiveDate::from_yo_opt(yyyy, doy.max(1)) {
Some(d) => d,
None => return 0.0,
};
let mjd_ref = chrono::NaiveDate::from_ymd_opt(1858, 11, 17).expect("valid MJD epoch");
let days = (epoch - mjd_ref).num_days() as f64;
days + sod / 86400.0
}