use core::fmt::Write as _;
use crate::astro::constants::time::{SECONDS_PER_DAY_I64, SECONDS_PER_HOUR};
use crate::astro::time::civil::civil_from_julian_day_number;
use crate::astro::time::gnss::week_epoch_julian_day_number;
use crate::astro::time::model::TimeScale;
use crate::astro::time::scales::julian_day_number;
use crate::id::GnssSystem;
use super::{BroadcastRecord, NavMessage};
pub fn encode_nav(records: &[BroadcastRecord]) -> String {
let mut out = String::with_capacity(64 + records.len() * 8 * 81);
let use_v4 = records.iter().any(|record| record.message.is_cnav_family());
write_header(&mut out, use_v4);
for record in records {
if use_v4 {
let _ = writeln!(
out,
"> EPH {} {}",
record.satellite_id,
message_token(record.message)
);
}
if record.message.is_cnav_family() {
write_cnav_record(&mut out, record);
} else {
write_record(&mut out, record);
}
}
out
}
fn write_header(out: &mut String, use_v4: bool) {
let version = if use_v4 { " 4.02" } else { " 3.04" };
let _ = writeln!(
out,
"{:<20}{:<40}{:<20}",
version, "N: GNSS NAV DATA M (MIXED)", "RINEX VERSION / TYPE"
);
let _ = writeln!(out, "{:<60}{:<20}", "", "END OF HEADER");
}
fn write_record(out: &mut String, record: &BroadcastRecord) {
let sat = record.satellite_id;
let system = sat.system;
let (year, month, day, hour, minute, second) = clock_epoch_civil(record);
let sat_token = sat.to_string();
let _ = write!(
out,
"{sat_token:<3} {year:04} {month:02} {day:02} {hour:02} {minute:02} {second:02}",
);
push_d19_12(out, record.clock.af0);
push_d19_12(out, record.clock.af1);
push_d19_12(out, record.clock.af2);
out.push('\n');
let e = &record.elements;
write_orbit(
out,
[
f64::from(record.issue_of_data.issue),
e.crs,
e.delta_n,
e.m0,
],
);
write_orbit(out, [e.cuc, e.e, e.cus, e.sqrt_a]);
write_orbit(out, [e.toe_sow, e.cic, e.omega0, e.cis]);
write_orbit(out, [e.i0, e.crc, e.omega, e.omega_dot]);
write_orbit(
out,
[
e.idot,
data_source_word(system, record.message),
f64::from(record.week),
0.0,
],
);
write_orbit(
out,
[
record.sv_accuracy_m,
record.sv_health,
group_delay_field(record, 0),
group_delay_field(record, 1),
],
);
write_orbit(out, [0.0, fit_interval_hours(record), 0.0, 0.0]);
}
fn write_cnav_record(out: &mut String, record: &BroadcastRecord) {
let cnav = record.cnav.expect("CNAV-family record carries cnav data");
let (year, month, day, hour, minute, second) = clock_epoch_civil(record);
let sat_token = record.satellite_id.to_string();
let _ = write!(
out,
"{sat_token:<3} {year:04} {month:02} {day:02} {hour:02} {minute:02} {second:02}",
);
push_d19_12(out, record.clock.af0);
push_d19_12(out, record.clock.af1);
push_d19_12(out, record.clock.af2);
out.push('\n');
let e = &record.elements;
let gd = &record.group_delays;
write_orbit(out, [cnav.adot_m_s, e.crs, e.delta_n, e.m0]);
write_orbit(out, [e.cuc, e.e, e.cus, e.sqrt_a]);
write_orbit(out, [cnav.top.tow_s, e.cic, e.omega0, e.cis]);
write_orbit(out, [e.i0, e.crc, e.omega, e.omega_dot]);
write_orbit(
out,
[
e.idot,
cnav.delta_n0_dot_rad_s2,
f64::from(cnav.ura_ned0_index),
f64::from(cnav.ura_ned1_index),
],
);
write_orbit_opt(
out,
[
Some(f64::from(cnav.ura_ed_index)),
Some(record.sv_health),
gd.gps_tgd_s,
Some(f64::from(cnav.ura_ned2_index)),
],
);
write_orbit_opt(
out,
[
gd.cnav_isc_l1ca_s,
gd.cnav_isc_l2c_s,
gd.cnav_isc_l5i5_s,
gd.cnav_isc_l5q5_s,
],
);
if matches!(record.message, NavMessage::GpsCnav2 | NavMessage::QzssCnav2) {
write_orbit_opt(out, [gd.cnav_isc_l1cd_s, gd.cnav_isc_l1cp_s, None, None]);
write_orbit_opt(
out,
[
Some(cnav.transmission_time_sow),
Some(f64::from(cnav.top.week)),
cnav.flags.map(f64::from),
None,
],
);
} else {
write_orbit_opt(
out,
[
Some(cnav.transmission_time_sow),
Some(f64::from(cnav.top.week)),
cnav.flags.map(f64::from),
None,
],
);
}
}
fn clock_epoch_civil(record: &BroadcastRecord) -> (i64, i64, i64, i64, i64, i64) {
let week = i64::from(record.toc.week);
let sow = record.toc.tow_s.round() as i64;
let base_jdn = week_epoch_jdn(record.toc.system);
let total_jdn = base_jdn + week * 7 + sow.div_euclid(SECONDS_PER_DAY_I64);
let tod = sow.rem_euclid(SECONDS_PER_DAY_I64);
let (year, month, day) = civil_from_julian_day_number(total_jdn);
let hour = tod / 3600;
let minute = (tod % 3600) / 60;
let second = tod % 60;
(year, month, day, hour, minute, second)
}
fn week_epoch_jdn(scale: TimeScale) -> i64 {
week_epoch_julian_day_number(scale).unwrap_or_else(|| julian_day_number(1980, 1, 6))
}
fn data_source_word(system: GnssSystem, message: NavMessage) -> f64 {
if system != GnssSystem::Galileo {
return 0.0;
}
match message {
NavMessage::GalileoFnav => 2.0,
_ => 1.0,
}
}
fn group_delay_field(record: &BroadcastRecord, index: usize) -> f64 {
use super::BroadcastGroupDelayTerm as T;
let gd = &record.group_delays;
let term = match (record.satellite_id.system, index) {
(GnssSystem::Gps, 0) => Some(T::GpsTgd),
(GnssSystem::Galileo, 0) => Some(T::GalileoBgdE5aE1),
(GnssSystem::Galileo, 1) => Some(T::GalileoBgdE5bE1),
(GnssSystem::BeiDou, 0) => Some(T::BeidouTgd1),
(GnssSystem::BeiDou, 1) => Some(T::BeidouTgd2),
_ => None,
};
term.and_then(|t| gd.get(t)).unwrap_or(0.0)
}
fn fit_interval_hours(record: &BroadcastRecord) -> f64 {
match (record.satellite_id.system, record.fit_interval_s) {
(GnssSystem::Gps, Some(seconds)) => seconds / SECONDS_PER_HOUR,
_ => 0.0,
}
}
fn write_orbit(out: &mut String, values: [f64; 4]) {
out.push_str(" ");
for value in values {
push_d19_12(out, value);
}
out.push('\n');
}
fn write_orbit_opt(out: &mut String, values: [Option<f64>; 4]) {
out.push_str(" ");
for value in values {
match value {
Some(value) => push_d19_12(out, value),
None => out.push_str(" "),
}
}
out.push('\n');
}
fn message_token(message: NavMessage) -> &'static str {
match message {
NavMessage::GpsLnav => "LNAV",
NavMessage::GpsCnav | NavMessage::QzssCnav => "CNAV",
NavMessage::GpsCnav2 | NavMessage::QzssCnav2 => "CNV2",
NavMessage::GalileoInav => "INAV",
NavMessage::GalileoFnav => "FNAV",
NavMessage::BeidouD1 => "D1",
NavMessage::BeidouD2 => "D2",
}
}
pub(super) fn push_d19_12(out: &mut String, value: f64) {
let negative = value.is_sign_negative() && value != 0.0;
let magnitude = value.abs();
let base = format!("{magnitude:.12e}");
let (mantissa, _) = base.split_once('e').expect("scientific form has 'e'");
let exponent = d19_12_exponent(value);
let sign = if negative { '-' } else { ' ' };
let _ = write!(out, "{sign}{mantissa}e{exponent:+03}");
}
fn d19_12_exponent(value: f64) -> i32 {
let base = format!("{:.12e}", value.abs());
let (_, exponent) = base.split_once('e').expect("scientific form has 'e'");
exponent.parse().expect("scientific exponent parses")
}
pub(super) fn d19_12_representable(value: f64) -> bool {
d19_12_exponent(value).unsigned_abs() <= 99
}