use core::fmt::Write as _;
use crate::id::GnssSystem;
use super::{ObsEpoch, ObsEpochTime, ObsValue, RinexObs, OBS_FIELD_WIDTH, OBS_VALUE_WIDTH};
const OBS_CODES_PER_LINE: usize = 13;
const GLONASS_SLOTS_PER_LINE: usize = 8;
impl RinexObs {
pub fn to_rinex_string(&self) -> String {
let mut out = String::new();
self.write_header(&mut out);
self.write_body(&mut out);
out
}
fn write_header(&self, out: &mut String) {
let h = &self.header;
push_header_line(
out,
&format!(
"{:<20}{:<40}",
format!("{:.2}", h.version),
"OBSERVATION DATA M (MIXED)"
),
"RINEX VERSION / TYPE",
);
if let Some(name) = &h.marker_name {
push_header_line(out, &format!("{name:<60}"), "MARKER NAME");
}
if let Some(pos) = h.approx_position_m {
push_header_line(out, &format_vec3(pos), "APPROX POSITION XYZ");
}
if let Some(delta) = h.antenna_delta_hen_m {
push_header_line(out, &format_vec3(delta), "ANTENNA: DELTA H/E/N");
}
for (system, codes) in &h.obs_codes {
write_obs_types(out, *system, codes);
}
if let Some(interval) = h.interval_s {
push_header_line(out, &format!("{interval:10.3}"), "INTERVAL");
}
if let Some((epoch, scale)) = h.time_of_first_obs {
let label = crate::rinex_common::time_scale_rinex_label(scale);
push_header_line(out, &format_first_obs(epoch, label), "TIME OF FIRST OBS");
}
for shift in &h.phase_shifts {
write_phase_shift(out, shift);
}
for factor in &h.scale_factors {
write_scale_factor(out, factor);
}
if !h.glonass_slots.is_empty() {
write_glonass_slots(out, &h.glonass_slots);
}
push_header_line(out, "", "END OF HEADER");
}
fn write_body(&self, out: &mut String) {
for epoch in &self.epochs {
self.write_epoch(out, epoch);
}
}
fn write_epoch(&self, out: &mut String, epoch: &ObsEpoch) {
let t = epoch.epoch;
let count = if epoch.flag > 1 { 0 } else { epoch.sats.len() };
let _ = writeln!(
out,
"> {:04} {:02} {:02} {:02} {:02}{:11.7} {}{:3}",
t.year, t.month, t.day, t.hour, t.minute, t.second, epoch.flag, count
);
if epoch.flag > 1 {
return;
}
for (sat, values) in &epoch.sats {
self.write_sat_record(out, *sat, values);
}
}
fn write_sat_record(
&self,
out: &mut String,
sat: crate::id::GnssSatelliteId,
values: &[ObsValue],
) {
let codes = self.header.obs_codes.get(&sat.system).map(Vec::as_slice);
let mut line = format!("{:<3}", sat.to_string());
for (index, value) in values.iter().enumerate() {
let code = codes.and_then(|c| c.get(index)).map(String::as_str);
push_obs_value(&mut line, *value, self.scale_for(sat.system, code));
}
let trimmed = line.trim_end();
out.push_str(trimmed);
out.push('\n');
}
fn scale_for(&self, system: GnssSystem, code: Option<&str>) -> f64 {
let Some(code) = code else {
return 1.0;
};
self.header
.scale_factors
.iter()
.rev()
.find(|record| {
record.system == system
&& (record.codes.is_empty() || record.codes.iter().any(|c| c == code))
})
.map_or(1.0, |record| record.factor)
}
}
fn push_header_line(out: &mut String, content: &str, label: &str) {
let _ = writeln!(out, "{content:<60}{label}");
}
fn format_vec3(values: [f64; 3]) -> String {
format!("{:14.4}{:14.4}{:14.4}", values[0], values[1], values[2])
}
fn format_first_obs(epoch: ObsEpochTime, scale_label: &str) -> String {
format!(
"{:6}{:6}{:6}{:6}{:6}{:13.7}{:>8}",
epoch.year, epoch.month, epoch.day, epoch.hour, epoch.minute, epoch.second, scale_label
)
}
fn write_obs_types(out: &mut String, system: GnssSystem, codes: &[String]) {
let count = codes.len();
for (chunk_index, chunk) in codes.chunks(OBS_CODES_PER_LINE).enumerate() {
let mut content = if chunk_index == 0 {
format!("{} {:>3}", system.letter(), count)
} else {
" ".repeat(6)
};
for code in chunk {
let _ = write!(content, " {code:>3}");
}
push_header_line(out, &content, "SYS / # / OBS TYPES");
}
if codes.is_empty() {
push_header_line(
out,
&format!("{} {:>3}", system.letter(), count),
"SYS / # / OBS TYPES",
);
}
}
fn write_phase_shift(out: &mut String, shift: &super::ObsPhaseShift) {
let mut content = format!(
"{} {} {}",
shift.system.letter(),
shift.code,
fmt_shortest(shift.correction_cycles)
);
if !shift.satellites.is_empty() {
let _ = write!(content, " {}", shift.satellites.len());
for sat in &shift.satellites {
let _ = write!(content, " {sat}");
}
}
push_header_line(out, &content, "SYS / PHASE SHIFT");
}
fn write_scale_factor(out: &mut String, factor: &super::ObsScaleFactor) {
let divisor = factor.factor as u32;
let count = factor.codes.len();
if factor.codes.is_empty() {
push_header_line(
out,
&format!("{} {:>4} {:>2}", factor.system.letter(), divisor, count),
"SYS / SCALE FACTOR",
);
return;
}
for (chunk_index, chunk) in factor.codes.chunks(OBS_CODES_PER_LINE).enumerate() {
let mut content = if chunk_index == 0 {
format!("{} {:>4} {:>2}", factor.system.letter(), divisor, count)
} else {
" ".repeat(10)
};
for code in chunk {
let _ = write!(content, " {code:>3}");
}
push_header_line(out, &content, "SYS / SCALE FACTOR");
}
}
fn write_glonass_slots(out: &mut String, slots: &std::collections::BTreeMap<u8, i8>) {
let entries: Vec<(u8, i8)> = slots
.iter()
.map(|(&prn, &channel)| (prn, channel))
.collect();
let count = entries.len();
for (chunk_index, chunk) in entries.chunks(GLONASS_SLOTS_PER_LINE).enumerate() {
let mut content = if chunk_index == 0 {
format!("{count:3} ")
} else {
" ".repeat(4)
};
for (prn, channel) in chunk {
let _ = write!(content, "R{prn:02} {channel:2} ");
}
push_header_line(out, content.trim_end(), "GLONASS SLOT / FRQ #");
}
}
fn push_obs_value(line: &mut String, value: ObsValue, scale: f64) {
match value.value {
Some(v) => {
let _ = write!(line, "{:width$.3}", v * scale, width = OBS_VALUE_WIDTH);
}
None => line.push_str(&" ".repeat(OBS_VALUE_WIDTH)),
}
push_indicator(line, value.lli);
push_indicator(line, value.ssi);
}
fn push_indicator(line: &mut String, indicator: Option<u8>) {
match indicator {
Some(digit) => {
let _ = write!(line, "{digit}");
}
None => line.push(' '),
}
}
fn fmt_shortest(value: f64) -> String {
format!("{value}")
}
const _: () = assert!(OBS_FIELD_WIDTH == OBS_VALUE_WIDTH + 2);