use crate::rinex::{col, orbit_fields, parse_d, EpochUtc};
use serde::Serialize;
const MU: f64 = 398600.4418;
const A_E: f64 = 6378.136;
const C20: f64 = -1.08262575e-3;
const OMEGA: f64 = 7.292115e-5;
const MAX_STEP_S: f64 = 60.0;
#[derive(Clone, Copy, Debug, Serialize)]
pub struct GlonassEphemeris {
pub prn: u8,
pub epoch: EpochUtc,
pub minus_tau_n: f64,
pub gamma_n: f64,
pub message_time_s: f64,
pub pos_m: [f64; 3],
pub vel_m_s: [f64; 3],
pub acc_m_s2: [f64; 3],
pub health: f64,
pub freq_channel: f64,
pub age_days: f64,
}
fn derivative(s: &[f64; 6], acc: [f64; 3]) -> [f64; 6] {
let (x, y, z, vx, vy, vz) = (s[0], s[1], s[2], s[3], s[4], s[5]);
let r2 = x * x + y * y + z * z;
let r = r2.sqrt();
let r3 = r2 * r;
let r5 = r3 * r2;
let j2 = 1.5 * C20 * MU * A_E * A_E / r5;
let z2r2 = z * z / r2;
let w2 = OMEGA * OMEGA;
[
vx,
vy,
vz,
-MU * x / r3 + j2 * x * (1.0 - 5.0 * z2r2) + w2 * x + 2.0 * OMEGA * vy + acc[0],
-MU * y / r3 + j2 * y * (1.0 - 5.0 * z2r2) + w2 * y - 2.0 * OMEGA * vx + acc[1],
-MU * z / r3 + j2 * z * (3.0 - 5.0 * z2r2) + acc[2],
]
}
fn rk4_step(s: &[f64; 6], h: f64, acc: [f64; 3]) -> [f64; 6] {
let advance = |base: &[f64; 6], k: &[f64; 6], f: f64| {
let mut o = [0.0; 6];
for i in 0..6 {
o[i] = base[i] + k[i] * f;
}
o
};
let k1 = derivative(s, acc);
let k2 = derivative(&advance(s, &k1, h * 0.5), acc);
let k3 = derivative(&advance(s, &k2, h * 0.5), acc);
let k4 = derivative(&advance(s, &k3, h), acc);
let mut out = [0.0; 6];
for i in 0..6 {
out[i] = s[i] + h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
}
out
}
impl GlonassEphemeris {
pub fn propagate(&self, t_offset_s: f64) -> ([f64; 3], [f64; 3]) {
let mut s = [
self.pos_m[0] / 1000.0,
self.pos_m[1] / 1000.0,
self.pos_m[2] / 1000.0,
self.vel_m_s[0] / 1000.0,
self.vel_m_s[1] / 1000.0,
self.vel_m_s[2] / 1000.0,
];
let acc = [
self.acc_m_s2[0] / 1000.0,
self.acc_m_s2[1] / 1000.0,
self.acc_m_s2[2] / 1000.0,
];
let n = (t_offset_s.abs() / MAX_STEP_S).ceil().max(1.0) as usize;
let h = t_offset_s / n as f64;
for _ in 0..n {
s = rk4_step(&s, h, acc);
}
(
[s[0] * 1000.0, s[1] * 1000.0, s[2] * 1000.0],
[s[3] * 1000.0, s[4] * 1000.0, s[5] * 1000.0],
)
}
pub fn position_ecef(&self, t_offset_s: f64) -> [f64; 3] {
self.propagate(t_offset_s).0
}
pub fn jd_ut1(&self, t_offset_s: f64) -> f64 {
crate::timescales::julian_date(
self.epoch.year,
self.epoch.month,
self.epoch.day,
self.epoch.hour,
self.epoch.minute,
self.epoch.second,
) + t_offset_s / 86_400.0
}
pub fn position_teme(&self, t_offset_s: f64) -> [f64; 3] {
crate::frames::ecef_to_teme(self.position_ecef(t_offset_s), self.jd_ut1(t_offset_s))
}
pub fn orbital_period_s(&self) -> f64 {
let r = (self.pos_m[0].powi(2) + self.pos_m[1].powi(2) + self.pos_m[2].powi(2)).sqrt();
std::f64::consts::TAU * (r * r * r / crate::orbit::MU_EARTH).sqrt()
}
}
pub fn parse_glonass_nav(text: &str) -> Result<Vec<GlonassEphemeris>, String> {
let lines: Vec<&str> = text.lines().collect();
let mut i = 0;
while i < lines.len() {
let done = lines[i].contains("END OF HEADER");
i += 1;
if done {
break;
}
}
let mut out = Vec::new();
while i < lines.len() {
let head = lines[i];
if head.trim().is_empty() {
i += 1;
continue;
}
let system = head.chars().next().unwrap_or(' ');
let nlines = if matches!(system, 'R' | 'S') { 4 } else { 8 };
if i + nlines > lines.len() {
break;
}
if system != 'R' {
i += nlines;
continue;
}
let prn: u8 = col(head, 1, 3)
.trim()
.parse()
.map_err(|_| format!("bad GLONASS PRN in {head:?}"))?;
let epoch = EpochUtc {
year: col(head, 4, 8).trim().parse().map_err(|_| "bad year")?,
month: col(head, 9, 11).trim().parse().map_err(|_| "bad month")?,
day: col(head, 12, 14).trim().parse().map_err(|_| "bad day")?,
hour: col(head, 15, 17).trim().parse().map_err(|_| "bad hour")?,
minute: col(head, 18, 20).trim().parse().map_err(|_| "bad minute")?,
second: parse_d(col(head, 21, 23))?,
};
let minus_tau_n = parse_d(col(head, 23, 42))?;
let gamma_n = parse_d(col(head, 42, 61))?;
let message_time_s = parse_d(col(head, 61, 80))?;
let l1 = orbit_fields(lines[i + 1])?;
let l2 = orbit_fields(lines[i + 2])?;
let l3 = orbit_fields(lines[i + 3])?;
out.push(GlonassEphemeris {
prn,
epoch,
minus_tau_n,
gamma_n,
message_time_s,
pos_m: [l1[0] * 1000.0, l2[0] * 1000.0, l3[0] * 1000.0],
vel_m_s: [l1[1] * 1000.0, l2[1] * 1000.0, l3[1] * 1000.0],
acc_m_s2: [l1[2] * 1000.0, l2[2] * 1000.0, l3[2] * 1000.0],
health: l1[3],
freq_channel: l2[3],
age_days: l3[3],
});
i += nlines;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "\
3.04 N: GNSS NAV DATA R: GLONASS RINEX VERSION / TYPE
END OF HEADER
R01 2023 01 01 00 15 00-1.234567890123D-04 0.000000000000D+00 9.000000000000D+02
7.150123046875D+03 2.500000000000D+00 9.313225746155D-10 0.000000000000D+00
-1.512345678901D+04 2.800000000000D+00 0.000000000000D+00 1.000000000000D+00
1.890123456789D+04 1.300000000000D+00-1.862645149231D-09 0.000000000000D+00";
#[test]
fn parses_a_glonass_record() {
let ephs = parse_glonass_nav(SAMPLE).expect("parses");
assert_eq!(ephs.len(), 1);
let e = &ephs[0];
assert_eq!(e.prn, 1);
assert_eq!(e.epoch.minute, 15);
assert!((e.pos_m[0] - 7150123.046875).abs() < 1e-3);
assert!((e.pos_m[1] - -15123456.78901).abs() < 1.0);
assert!((e.pos_m[2] - 18901234.56789).abs() < 1.0);
assert!((e.vel_m_s[0] - 2500.0).abs() < 1e-6);
let r = (e.pos_m[0].powi(2) + e.pos_m[1].powi(2) + e.pos_m[2].powi(2)).sqrt();
assert!((r - 25_500_000.0).abs() < 1_000_000.0, "radius {r:.0} m");
}
#[test]
fn position_at_epoch_is_the_broadcast_state() {
let e = &parse_glonass_nav(SAMPLE).unwrap()[0];
assert_eq!(e.position_ecef(0.0), e.pos_m);
}
#[test]
fn integration_keeps_the_orbit_radius_physical() {
let e = &parse_glonass_nav(SAMPLE).unwrap()[0];
let r0 = (e.pos_m[0].powi(2) + e.pos_m[1].powi(2) + e.pos_m[2].powi(2)).sqrt();
let p = e.position_ecef(300.0);
let r = (p[0].powi(2) + p[1].powi(2) + p[2].powi(2)).sqrt();
assert!(
(r - r0).abs() / r0 < 0.05,
"radius drift {:.3}",
(r - r0) / r0
);
}
#[test]
fn integration_is_reversible() {
let e = &parse_glonass_nav(SAMPLE).unwrap()[0];
let (p_fwd, v_fwd) = e.propagate(600.0);
let mut forward = *e;
forward.pos_m = p_fwd;
forward.vel_m_s = v_fwd;
let back = forward.position_ecef(-600.0);
for (got, want) in back.iter().zip(e.pos_m.iter()) {
assert!(
(got - want).abs() < 1.0,
"round-trip error {} m",
got - want
);
}
}
#[test]
fn skips_non_glonass_records() {
let gps = "G01 2023 01 01 00 00 00 0.0D+00 0.0D+00 0.0D+00\n \
0.0D+00 0.0D+00 0.0D+00 0.0D+00\n 0.0D+00 0.0D+00 0.0D+00 0.0D+00\n \
0.0D+00 0.0D+00 0.0D+00 0.0D+00\n 0.0D+00 0.0D+00 0.0D+00 0.0D+00\n \
0.0D+00 0.0D+00 0.0D+00 0.0D+00\n 0.0D+00 0.0D+00 0.0D+00 0.0D+00\n \
0.0D+00 0.0D+00 0.0D+00 0.0D+00";
let mixed = SAMPLE.replace(
"R01 2023 01 01 00 15 00",
&format!("{gps}\nR01 2023 01 01 00 15 00"),
);
let ephs = parse_glonass_nav(&mixed).expect("parses");
assert_eq!(ephs.len(), 1);
assert_eq!(ephs[0].prn, 1);
}
}