mod store;
pub use store::BroadcastStore;
mod write;
pub use write::encode_nav;
use crate::astro::time::model::{GnssWeekTow, TimeScale};
use crate::astro::time::{civil, gnss};
use crate::broadcast::{ClockPolynomial, ConstellationConstants, KeplerianElements};
use crate::constants::KM_TO_M;
use crate::format::columns::{field, raw_field};
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::ionex::GalileoNequickCoeffs;
use crate::validate::{self, FieldError};
fn parse_f64(line: &str, start: usize, end: usize) -> Option<f64> {
let value = crate::format::columns::fortran_f64(line, start, end, "numeric field")?;
write::d19_12_representable(value).then_some(value)
}
pub(crate) const MAX_EPHEMERIS_AGE_S: f64 = 4.0 * 3600.0;
pub(crate) const GLONASS_MAX_AGE_S: f64 = 15.0 * 60.0;
const GPS_NOMINAL_FIT_INTERVAL_S: f64 = 4.0 * 3600.0;
const GPS_LEGACY_EXTENDED_FIT_INTERVAL_S: f64 = 8.0 * 3600.0;
const GLONASS_FREQ_CHANNEL_MIN: i32 = -7;
const GLONASS_FREQ_CHANNEL_MAX: i32 = 6;
pub(crate) fn valid_glonass_frequency_channel(channel: i32) -> bool {
(GLONASS_FREQ_CHANNEL_MIN..=GLONASS_FREQ_CHANNEL_MAX).contains(&channel)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RinexVersion {
major: u8,
minor: u8,
}
impl RinexVersion {
fn gps_fit_interval_uses_legacy_flag(self) -> bool {
self.major == 3 && self.minor <= 2
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavMessage {
GpsLnav,
GalileoInav,
GalileoFnav,
BeidouD1,
BeidouD2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BroadcastGroupDelayTerm {
GpsTgd,
GalileoBgdE5aE1,
GalileoBgdE5bE1,
BeidouTgd1,
BeidouTgd2,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct BroadcastGroupDelays {
pub gps_tgd_s: Option<f64>,
pub galileo_bgd_e5a_e1_s: Option<f64>,
pub galileo_bgd_e5b_e1_s: Option<f64>,
pub beidou_tgd1_s: Option<f64>,
pub beidou_tgd2_s: Option<f64>,
}
impl BroadcastGroupDelays {
pub const fn gps_lnav(tgd_s: f64) -> Self {
Self {
gps_tgd_s: Some(tgd_s),
galileo_bgd_e5a_e1_s: None,
galileo_bgd_e5b_e1_s: None,
beidou_tgd1_s: None,
beidou_tgd2_s: None,
}
}
pub const fn galileo(bgd_e5a_e1_s: f64, bgd_e5b_e1_s: f64) -> Self {
Self {
gps_tgd_s: None,
galileo_bgd_e5a_e1_s: Some(bgd_e5a_e1_s),
galileo_bgd_e5b_e1_s: Some(bgd_e5b_e1_s),
beidou_tgd1_s: None,
beidou_tgd2_s: None,
}
}
pub const fn beidou(tgd1_s: f64, tgd2_s: f64) -> Self {
Self {
gps_tgd_s: None,
galileo_bgd_e5a_e1_s: None,
galileo_bgd_e5b_e1_s: None,
beidou_tgd1_s: Some(tgd1_s),
beidou_tgd2_s: Some(tgd2_s),
}
}
pub const fn get(&self, term: BroadcastGroupDelayTerm) -> Option<f64> {
match term {
BroadcastGroupDelayTerm::GpsTgd => self.gps_tgd_s,
BroadcastGroupDelayTerm::GalileoBgdE5aE1 => self.galileo_bgd_e5a_e1_s,
BroadcastGroupDelayTerm::GalileoBgdE5bE1 => self.galileo_bgd_e5b_e1_s,
BroadcastGroupDelayTerm::BeidouTgd1 => self.beidou_tgd1_s,
BroadcastGroupDelayTerm::BeidouTgd2 => self.beidou_tgd2_s,
}
}
pub const fn for_message(self, system: GnssSystem, message: NavMessage) -> Option<f64> {
match (system, message) {
(GnssSystem::Gps, NavMessage::GpsLnav) => self.get(BroadcastGroupDelayTerm::GpsTgd),
(GnssSystem::Galileo, NavMessage::GalileoFnav) => {
self.get(BroadcastGroupDelayTerm::GalileoBgdE5aE1)
}
(GnssSystem::Galileo, NavMessage::GalileoInav) => {
self.get(BroadcastGroupDelayTerm::GalileoBgdE5bE1)
}
(GnssSystem::BeiDou, NavMessage::BeidouD1 | NavMessage::BeidouD2) => {
self.get(BroadcastGroupDelayTerm::BeidouTgd1)
}
_ => None,
}
}
}
pub fn is_beidou_geo(sat: GnssSatelliteId) -> bool {
sat.system == GnssSystem::BeiDou && (sat.prn <= 5 || (59..=61).contains(&sat.prn))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KlobucharAlphaBeta {
pub alpha: [f64; 4],
pub beta: [f64; 4],
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct IonoCorrections {
pub gps: Option<KlobucharAlphaBeta>,
pub beidou: Option<KlobucharAlphaBeta>,
pub galileo: Option<GalileoNequickCoeffs>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlonassRecord {
pub satellite_id: GnssSatelliteId,
pub toe_utc_j2000_s: f64,
pub pos_m: [f64; 3],
pub vel_m_s: [f64; 3],
pub acc_m_s2: [f64; 3],
pub clk_bias: f64,
pub gamma_n: f64,
pub sv_health: f64,
pub freq_channel: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedGlonass {
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GlonassParse {
pub records: Vec<GlonassRecord>,
pub skipped: Vec<SkippedGlonass>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BroadcastRecord {
pub satellite_id: GnssSatelliteId,
pub message: NavMessage,
pub week: u32,
pub toe: GnssWeekTow,
pub toc: GnssWeekTow,
pub elements: KeplerianElements,
pub clock: ClockPolynomial,
pub group_delays: BroadcastGroupDelays,
pub sv_health: f64,
pub sv_accuracy_m: f64,
pub fit_interval_s: Option<f64>,
}
impl BroadcastRecord {
pub const fn time_scale(&self) -> TimeScale {
self.toe.system
}
pub const fn constants(&self) -> ConstellationConstants {
match self.satellite_id.system {
GnssSystem::Galileo => ConstellationConstants::GALILEO,
GnssSystem::BeiDou => ConstellationConstants::BEIDOU,
_ => ConstellationConstants::GPS,
}
}
pub fn broadcast_clock_group_delay_s(&self) -> f64 {
self.group_delays
.for_message(self.satellite_id.system, self.message)
.unwrap_or(0.0)
}
pub fn from_lnav(
decoded: &crate::navigation::lnav::LnavDecoded,
satellite_id: GnssSatelliteId,
full_week: u32,
) -> Result<Self, LnavRecordError> {
if satellite_id.system != GnssSystem::Gps {
return Err(LnavRecordError::NotGps(satellite_id));
}
if i64::from(full_week % 1024) != decoded.week_number {
return Err(LnavRecordError::WeekMismatch {
full_week,
decoded_week: decoded.week_number,
});
}
let sv_accuracy_m = gps_ura_index_to_meters(decoded.ura_index)
.ok_or(LnavRecordError::NoUraPrediction(decoded.ura_index))?;
let fit_interval_s =
gps_fit_interval_from_flag(decoded.fit_interval_flag, decoded.iode, decoded.iodc)?;
const SEMICIRCLE_TO_RAD: f64 = core::f64::consts::PI;
let elements = KeplerianElements {
sqrt_a: decoded.sqrt_a,
e: decoded.eccentricity,
m0: decoded.m0 * SEMICIRCLE_TO_RAD,
delta_n: decoded.delta_n * SEMICIRCLE_TO_RAD,
omega0: decoded.omega0 * SEMICIRCLE_TO_RAD,
i0: decoded.i0 * SEMICIRCLE_TO_RAD,
omega: decoded.omega * SEMICIRCLE_TO_RAD,
omega_dot: decoded.omega_dot * SEMICIRCLE_TO_RAD,
idot: decoded.idot * SEMICIRCLE_TO_RAD,
cuc: decoded.cuc,
cus: decoded.cus,
crc: decoded.crc,
crs: decoded.crs,
cic: decoded.cic,
cis: decoded.cis,
toe_sow: decoded.toe as f64,
};
let clock = ClockPolynomial {
af0: decoded.af0,
af1: decoded.af1,
af2: decoded.af2,
toc_sow: decoded.toc as f64,
};
let toe = GnssWeekTow::new(TimeScale::Gpst, full_week, elements.toe_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| LnavRecordError::InvalidEpoch("toe"))?;
let toc = GnssWeekTow::new(TimeScale::Gpst, full_week, clock.toc_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| LnavRecordError::InvalidEpoch("toc"))?;
Ok(BroadcastRecord {
satellite_id,
message: NavMessage::GpsLnav,
week: full_week,
toe,
toc,
elements,
clock,
group_delays: BroadcastGroupDelays::gps_lnav(decoded.tgd),
sv_health: decoded.sv_health as f64,
sv_accuracy_m,
fit_interval_s: Some(fit_interval_s),
})
}
}
fn gps_ura_index_to_meters(index: i64) -> Option<f64> {
let meters = match index {
0 => 2.4,
1 => 3.4,
2 => 4.85,
3 => 6.85,
4 => 9.65,
5 => 13.65,
6 => 24.0,
7 => 48.0,
8 => 96.0,
9 => 192.0,
10 => 384.0,
11 => 768.0,
12 => 1536.0,
13 => 3072.0,
14 => 6144.0,
_ => return None,
};
Some(meters)
}
const GPS_FIT_INTERVAL_6H_S: f64 = 6.0 * 3600.0;
const GPS_FIT_INTERVAL_8H_S: f64 = 8.0 * 3600.0;
const GPS_FIT_INTERVAL_14H_S: f64 = 14.0 * 3600.0;
const GPS_FIT_INTERVAL_26H_S: f64 = 26.0 * 3600.0;
fn gps_fit_interval_from_flag(
fit_interval_flag: i64,
iode: i64,
iodc: i64,
) -> Result<f64, LnavRecordError> {
let unsupported = || LnavRecordError::FitIntervalUnsupported {
fit_interval_flag,
iode,
iodc,
};
match fit_interval_flag {
0 => Ok(GPS_NOMINAL_FIT_INTERVAL_S),
1 => {
if (0..240).contains(&iode) {
Ok(GPS_FIT_INTERVAL_6H_S)
} else if (240..=255).contains(&iode) {
match iodc {
240..=247 => Ok(GPS_FIT_INTERVAL_8H_S),
248..=255 | 496 => Ok(GPS_FIT_INTERVAL_14H_S),
497..=503 | 1021..=1023 => Ok(GPS_FIT_INTERVAL_26H_S),
_ => Err(unsupported()),
}
} else {
Err(unsupported())
}
}
_ => Err(unsupported()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LnavRecordError {
NotGps(GnssSatelliteId),
InvalidEpoch(&'static str),
WeekMismatch {
full_week: u32,
decoded_week: i64,
},
NoUraPrediction(i64),
FitIntervalUnsupported {
fit_interval_flag: i64,
iode: i64,
iodc: i64,
},
}
impl core::fmt::Display for LnavRecordError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
LnavRecordError::NotGps(sat) => {
write!(f, "LNAV is a GPS message; {sat} is not a GPS satellite")
}
LnavRecordError::InvalidEpoch(field) => {
write!(f, "derived {field} week/TOW is not representable")
}
LnavRecordError::WeekMismatch {
full_week,
decoded_week,
} => write!(
f,
"full_week {full_week} (week % 1024 = {}) disagrees with decoded 10-bit week {decoded_week}",
full_week % 1024
),
LnavRecordError::NoUraPrediction(index) => {
write!(f, "URA index {index} carries no accuracy prediction")
}
LnavRecordError::FitIntervalUnsupported {
fit_interval_flag,
iode,
iodc,
} => write!(
f,
"fit interval flag {fit_interval_flag} with IODE {iode} / IODC {iodc} is not a defined curve-fit interval"
),
}
}
}
impl std::error::Error for LnavRecordError {}
fn broadcast_time_scale(system: GnssSystem) -> TimeScale {
match system {
GnssSystem::Galileo => TimeScale::Gst,
GnssSystem::BeiDou => TimeScale::Bdt,
_ => TimeScale::Gpst,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NavParseError {
UnsupportedHeader(String),
MissingHeaderEnd,
TruncatedRecord(String),
BadField {
satellite: String,
field: &'static str,
},
BadHeaderField {
field: &'static str,
},
}
impl core::fmt::Display for NavParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
NavParseError::UnsupportedHeader(s) => write!(f, "unsupported RINEX NAV header: {s}"),
NavParseError::MissingHeaderEnd => write!(f, "no END OF HEADER line"),
NavParseError::TruncatedRecord(s) => write!(f, "truncated navigation record for {s}"),
NavParseError::BadField { satellite, field } => {
write!(f, "bad/missing {field} field in record for {satellite}")
}
NavParseError::BadHeaderField { field } => {
write!(f, "bad/missing {field} field in navigation header")
}
}
}
}
impl std::error::Error for NavParseError {}
pub fn parse_nav(text: &str) -> Result<Vec<BroadcastRecord>, NavParseError> {
let mut lines = text.lines();
let version = verify_and_skip_header(&mut lines)?;
if version.major >= 4 {
parse_nav_v4(lines, version)
} else {
parse_nav_v3(lines, version)
}
}
fn parse_nav_v3<'a, I>(
lines: I,
version: RinexVersion,
) -> Result<Vec<BroadcastRecord>, NavParseError>
where
I: Iterator<Item = &'a str>,
{
let mut blocks: Vec<Vec<&str>> = Vec::new();
for line in lines {
if is_record_start(line) {
blocks.push(vec![line]);
} else if let Some(last) = blocks.last_mut() {
last.push(line);
}
}
let mut records = Vec::new();
for block in &blocks {
let letter = block[0].as_bytes()[0] as char;
match GnssSystem::from_letter(letter) {
Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou) => {
records.push(parse_keplerian_block(block, None, version)?);
}
_ => {}
}
}
Ok(records)
}
fn parse_nav_v4<'a, I>(
lines: I,
version: RinexVersion,
) -> Result<Vec<BroadcastRecord>, NavParseError>
where
I: Iterator<Item = &'a str>,
{
let frames = v4_frames(lines);
let mut records = Vec::new();
for (marker, body) in &frames {
let Some((frame_type, sv, msg_token)) = parse_v4_marker(marker) else {
continue;
};
if frame_type != "EPH" {
continue; }
let letter = sv.as_bytes().first().copied().map_or(' ', char::from);
let supported = matches!(
GnssSystem::from_letter(letter),
Some(GnssSystem::Gps) | Some(GnssSystem::Galileo) | Some(GnssSystem::BeiDou)
);
if !supported {
continue; }
if let Some(message) = nav_message_from_v4_token(msg_token) {
validate_v4_ephemeris_marker(sv, message, body)?;
records.push(parse_keplerian_block(body, Some(message), version)?);
}
}
Ok(records)
}
fn v4_frames<'a, I>(lines: I) -> Vec<(&'a str, Vec<&'a str>)>
where
I: Iterator<Item = &'a str>,
{
let mut frames: Vec<(&str, Vec<&str>)> = Vec::new();
for line in lines {
if is_v4_frame_marker(line) {
frames.push((line, Vec::new()));
} else if let Some((_, body)) = frames.last_mut() {
body.push(line);
}
}
frames
}
fn is_v4_frame_marker(line: &str) -> bool {
line.starts_with("> ")
}
fn parse_v4_marker(line: &str) -> Option<(&str, &str, &str)> {
let rest = line.strip_prefix('>')?;
let mut fields = rest.split_whitespace();
let frame_type = fields.next()?;
let sv = fields.next()?;
let msg_token = fields.next()?;
Some((frame_type, sv, msg_token))
}
fn nav_message_from_v4_token(token: &str) -> Option<NavMessage> {
match token {
"LNAV" => Some(NavMessage::GpsLnav),
"INAV" => Some(NavMessage::GalileoInav),
"FNAV" => Some(NavMessage::GalileoFnav),
"D1" => Some(NavMessage::BeidouD1),
"D2" => Some(NavMessage::BeidouD2),
_ => None,
}
}
fn validate_v4_ephemeris_marker(
marker_sv: &str,
message: NavMessage,
body: &[&str],
) -> Result<(), NavParseError> {
let Some(body_sv) = body
.first()
.and_then(|line| line.get(0..3))
.map(str::trim)
.filter(|sv| !sv.is_empty())
else {
return Ok(());
};
if marker_sv != body_sv {
return Err(NavParseError::BadField {
satellite: marker_sv.to_string(),
field: "frame marker",
});
}
let system = body_sv
.as_bytes()
.first()
.and_then(|b| GnssSystem::from_letter(*b as char))
.ok_or_else(|| NavParseError::BadField {
satellite: body_sv.to_string(),
field: "system",
})?;
if !nav_message_matches_system(message, system) {
return Err(NavParseError::BadField {
satellite: body_sv.to_string(),
field: "message",
});
}
Ok(())
}
fn nav_message_matches_system(message: NavMessage, system: GnssSystem) -> bool {
matches!(
(message, system),
(NavMessage::GpsLnav, GnssSystem::Gps)
| (
NavMessage::GalileoInav | NavMessage::GalileoFnav,
GnssSystem::Galileo,
)
| (
NavMessage::BeidouD1 | NavMessage::BeidouD2,
GnssSystem::BeiDou,
)
)
}
pub fn parse_iono_corrections(text: &str) -> Result<IonoCorrections, NavParseError> {
parse_iono_corrections_checked(text)
}
fn parse_iono_corrections_checked(text: &str) -> Result<IonoCorrections, NavParseError> {
let klobuchar_row = |line: &str| -> Result<[f64; 4], NavParseError> {
Ok([
strict_header_f64(line, 5, 17, "ionospheric correction")?,
strict_header_f64(line, 17, 29, "ionospheric correction")?,
strict_header_f64(line, 29, 41, "ionospheric correction")?,
strict_header_f64(line, 41, 53, "ionospheric correction")?,
])
};
let nequick_row = |line: &str| -> Result<[f64; 3], NavParseError> {
Ok([
strict_header_f64(line, 5, 17, "ionospheric correction")?,
strict_header_f64(line, 17, 29, "ionospheric correction")?,
strict_header_f64(line, 29, 41, "ionospheric correction")?,
])
};
let (mut gpsa, mut gpsb, mut bdsa, mut bdsb, mut gal) = (None, None, None, None, None);
for line in text.lines() {
if line.contains("END OF HEADER") {
break;
}
if !line.contains("IONOSPHERIC CORR") {
continue;
}
match line.get(0..4).map(str::trim) {
Some("GPSA") => gpsa = Some(klobuchar_row(line)?),
Some("GPSB") => gpsb = Some(klobuchar_row(line)?),
Some("BDSA") => bdsa = Some(klobuchar_row(line)?),
Some("BDSB") => bdsb = Some(klobuchar_row(line)?),
Some("GAL") => {
let row = nequick_row(line)?;
gal = Some(GalileoNequickCoeffs {
ai0: row[0],
ai1: row[1],
ai2: row[2],
});
}
_ => {}
}
}
let pair = |a: Option<[f64; 4]>, b: Option<[f64; 4]>| match (a, b) {
(Some(alpha), Some(beta)) => Some(KlobucharAlphaBeta { alpha, beta }),
_ => None,
};
let mut iono = IonoCorrections {
gps: pair(gpsa, gpsb),
beidou: pair(bdsa, bdsb),
galileo: gal,
};
parse_v4_body_iono_corrections(text, &mut iono)?;
Ok(iono)
}
fn parse_v4_body_iono_corrections(
text: &str,
iono: &mut IonoCorrections,
) -> Result<(), NavParseError> {
let mut lines = text.lines();
for line in lines.by_ref() {
if line.contains("END OF HEADER") {
break;
}
}
for (marker, body) in v4_frames(lines) {
let Some((frame_type, sv, _msg_token)) = parse_v4_marker(marker) else {
continue;
};
if frame_type != "ION" {
continue;
}
let values = parse_v4_iono_values(sv, &body)?;
match sv
.as_bytes()
.first()
.and_then(|b| GnssSystem::from_letter(*b as char))
{
Some(GnssSystem::Gps) => {
iono.gps = Some(KlobucharAlphaBeta {
alpha: iono_values_4(&values, 0, sv)?,
beta: iono_values_4(&values, 4, sv)?,
});
}
Some(GnssSystem::BeiDou) => {
iono.beidou = Some(KlobucharAlphaBeta {
alpha: iono_values_4(&values, 0, sv)?,
beta: iono_values_4(&values, 4, sv)?,
});
}
Some(GnssSystem::Galileo) => {
let coeffs = iono_values_3(&values, 0, sv)?;
iono.galileo = Some(GalileoNequickCoeffs {
ai0: coeffs[0],
ai1: coeffs[1],
ai2: coeffs[2],
});
}
_ => {}
}
}
Ok(())
}
fn parse_v4_iono_values(sv: &str, body: &[&str]) -> Result<Vec<f64>, NavParseError> {
if body.is_empty() {
return Err(NavParseError::BadField {
satellite: sv.to_string(),
field: "ionospheric correction",
});
}
let mut values = Vec::new();
for (idx, line) in body.iter().enumerate() {
let ranges: &[(usize, usize)] = if idx == 0 {
&[(23, 42), (42, 61), (61, 80)]
} else {
&[(4, 23), (23, 42), (42, 61), (61, 80)]
};
for &(start, end) in ranges {
let raw = raw_field(line, start, end);
if raw.trim().is_empty() {
continue;
}
values.push(
validate::strict_f64(raw, "ionospheric correction")
.map_err(|error| map_record_field_error(error, sv))?,
);
}
}
Ok(values)
}
fn iono_values_4(values: &[f64], start: usize, sv: &str) -> Result<[f64; 4], NavParseError> {
let Some(slice) = values.get(start..start + 4) else {
return Err(NavParseError::BadField {
satellite: sv.to_string(),
field: "ionospheric correction",
});
};
Ok([slice[0], slice[1], slice[2], slice[3]])
}
fn iono_values_3(values: &[f64], start: usize, sv: &str) -> Result<[f64; 3], NavParseError> {
let Some(slice) = values.get(start..start + 3) else {
return Err(NavParseError::BadField {
satellite: sv.to_string(),
field: "ionospheric correction",
});
};
Ok([slice[0], slice[1], slice[2]])
}
pub fn parse_leap_seconds(text: &str) -> Result<Option<f64>, NavParseError> {
parse_leap_seconds_checked(text)
}
fn parse_leap_seconds_checked(text: &str) -> Result<Option<f64>, NavParseError> {
for line in text.lines() {
if line.contains("END OF HEADER") {
break;
}
if line.contains("LEAP SECONDS") {
return strict_header_integer_f64(line, 0, 6, "leap seconds").map(Some);
}
}
Ok(None)
}
fn j2000_seconds_utc(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> f64 {
civil::j2000_seconds(y as i32, mo as i32, d as i32, h as i32, mi as i32, s as f64)
}
fn parse_glonass_epoch(l0: &str, sat: &str) -> Result<f64, NavParseError> {
let year = strict_record_int::<i64>(l0, 4, 8, "epoch", sat)?;
let month = strict_record_int::<i64>(l0, 9, 11, "epoch", sat)?;
let day = strict_record_int::<i64>(l0, 12, 14, "epoch", sat)?;
let hour = strict_record_int::<i64>(l0, 15, 17, "epoch", sat)?;
let minute = strict_record_int::<i64>(l0, 18, 20, "epoch", sat)?;
let second = strict_record_int::<i64>(l0, 21, 23, "epoch", sat)?;
let civil = validate::civil_datetime_with_second_policy(
year,
month,
day,
hour,
minute,
second as f64,
validate::CivilSecondPolicy::UtcLike,
)
.map_err(|_| NavParseError::BadField {
satellite: sat.to_string(),
field: "epoch",
})?;
Ok(j2000_seconds_utc(
civil.year,
i64::from(civil.month),
i64::from(civil.day),
i64::from(civil.hour),
i64::from(civil.minute),
civil.second as i64,
))
}
fn parse_glonass_block(block: &[&str]) -> Result<GlonassRecord, NavParseError> {
let l0 = block[0];
let sat = l0.get(0..3).unwrap_or("").trim().to_string();
if block.len() < 4 {
return Err(NavParseError::TruncatedRecord(sat));
}
let bad = |what: &'static str| NavParseError::BadField {
satellite: sat.clone(),
field: what,
};
let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
let toe_utc_j2000_s = parse_glonass_epoch(l0, &sat)?;
let clk_bias = parse_f64(l0, 23, 42).ok_or_else(|| bad("clock bias"))?;
let gamma_n = parse_f64(l0, 42, 61).ok_or_else(|| bad("gamma_n"))?;
let o1 = orbit_row(block[1]);
let o2 = orbit_row(block[2]);
let o3 = orbit_row(block[3]);
let km = |v: Option<f64>, what: &'static str| v.map(|x| x * KM_TO_M).ok_or_else(|| bad(what));
let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
Ok(GlonassRecord {
satellite_id,
toe_utc_j2000_s,
pos_m: [km(o1[0], "x")?, km(o2[0], "y")?, km(o3[0], "z")?],
vel_m_s: [km(o1[1], "vx")?, km(o2[1], "vy")?, km(o3[1], "vz")?],
acc_m_s2: [km(o1[2], "ax")?, km(o2[2], "ay")?, km(o3[2], "az")?],
clk_bias,
gamma_n,
sv_health: g(o1[3], "health")?,
freq_channel: glonass_frequency_channel(g(o2[3], "frequency channel")?, &sat)?,
})
}
pub fn parse_glonass(text: &str) -> Result<Vec<GlonassRecord>, NavParseError> {
Ok(parse_glonass_lenient(text)?.records)
}
pub fn parse_glonass_lenient(text: &str) -> Result<GlonassParse, NavParseError> {
let mut lines = text.lines();
verify_and_skip_header(&mut lines)?;
let mut blocks: Vec<Vec<&str>> = Vec::new();
for line in lines {
if is_record_start(line) {
blocks.push(vec![line]);
} else if let Some(last) = blocks.last_mut() {
last.push(line);
}
}
let mut out = GlonassParse::default();
for block in blocks.iter().filter(|b| b[0].starts_with('R')) {
let sat = block[0].get(0..3).unwrap_or("").trim();
if sat.parse::<GnssSatelliteId>().is_err() {
out.skipped.push(SkippedGlonass {
token: sat.to_string(),
});
continue;
}
out.records.push(parse_glonass_block(block)?);
}
Ok(out)
}
fn verify_and_skip_header<'a, I>(lines: &mut I) -> Result<RinexVersion, NavParseError>
where
I: Iterator<Item = &'a str>,
{
let mut version_seen: Option<RinexVersion> = None;
for line in lines.by_ref() {
if line.contains("RINEX VERSION / TYPE") {
let version = line.get(0..9).unwrap_or("").trim();
let detected = parse_rinex_version(version);
let is_nav = line.get(20..21) == Some("N");
match (detected, is_nav) {
(Some(v), true) => version_seen = Some(v),
_ => {
return Err(NavParseError::UnsupportedHeader(
line.trim_end().to_string(),
))
}
}
}
if line.contains("END OF HEADER") {
return version_seen.ok_or_else(|| {
NavParseError::UnsupportedHeader("no RINEX VERSION / TYPE".to_string())
});
}
}
Err(NavParseError::MissingHeaderEnd)
}
fn parse_rinex_version(version: &str) -> Option<RinexVersion> {
let (major, minor) = version.split_once('.')?;
let major = major.trim().parse::<u8>().ok()?;
if !matches!(major, 3 | 4) {
return None;
}
let minor_digits = minor
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>();
if minor_digits.is_empty() {
return None;
}
let minor = minor_digits.parse::<u8>().ok()?;
Some(RinexVersion { major, minor })
}
fn is_record_start(line: &str) -> bool {
let b = line.as_bytes();
b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1].is_ascii_digit() && b[2].is_ascii_digit()
}
fn orbit_row(line: &str) -> [Option<f64>; 4] {
[
parse_f64(line, 4, 23),
parse_f64(line, 23, 42),
parse_f64(line, 42, 61),
parse_f64(line, 61, 80),
]
}
#[derive(Debug, Clone, Copy)]
struct ClockReferenceEpoch {
week: u32,
sow: f64,
}
fn parse_keplerian_block(
block: &[&str],
message_override: Option<NavMessage>,
version: RinexVersion,
) -> Result<BroadcastRecord, NavParseError> {
let l0 = block.first().copied().unwrap_or("");
let sat = l0.get(0..3).unwrap_or("").trim().to_string();
if block.len() < 8 {
return Err(NavParseError::TruncatedRecord(sat));
}
let bad = |what: &'static str| NavParseError::BadField {
satellite: sat.clone(),
field: what,
};
let letter = l0
.as_bytes()
.first()
.copied()
.map(|b| b as char)
.ok_or_else(|| bad("system"))?;
let system = GnssSystem::from_letter(letter).ok_or_else(|| bad("system"))?;
let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
let time_scale = broadcast_time_scale(system);
let toc_epoch = parse_toc(l0, &sat, time_scale)?;
let toc_sow = toc_epoch.sow;
let af0 = parse_f64(l0, 23, 42).ok_or_else(|| bad("af0"))?;
let af1 = parse_f64(l0, 42, 61).ok_or_else(|| bad("af1"))?;
let af2 = parse_f64(l0, 61, 80).ok_or_else(|| bad("af2"))?;
let o1 = orbit_row(block[1]);
let o2 = orbit_row(block[2]);
let o3 = orbit_row(block[3]);
let o4 = orbit_row(block[4]);
let o5 = orbit_row(block[5]);
let o6 = orbit_row(block[6]);
let g = |v: Option<f64>, what: &'static str| v.ok_or_else(|| bad(what));
let elements = KeplerianElements {
crs: g(o1[1], "crs")?,
delta_n: g(o1[2], "deltaN")?,
m0: g(o1[3], "m0")?,
cuc: g(o2[0], "cuc")?,
e: g(o2[1], "e")?,
cus: g(o2[2], "cus")?,
sqrt_a: g(o2[3], "sqrtA")?,
toe_sow: g(o3[0], "toe")?,
cic: g(o3[1], "cic")?,
omega0: g(o3[2], "omega0")?,
cis: g(o3[3], "cis")?,
i0: g(o4[0], "i0")?,
crc: g(o4[1], "crc")?,
omega: g(o4[2], "omega")?,
omega_dot: g(o4[3], "omegaDot")?,
idot: g(o5[0], "idot")?,
};
let clock = ClockPolynomial {
af0,
af1,
af2,
toc_sow,
};
let week = finite_integral_u32(g(o5[2], "week")?, "week", &sat)?;
let toe = GnssWeekTow::new(time_scale, week, elements.toe_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| bad("toe"))?;
let toc = GnssWeekTow::new(time_scale, toc_epoch.week, clock.toc_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| bad("toc"))?;
let message = if let Some(message) = message_override {
message
} else {
match system {
GnssSystem::Galileo => galileo_message(g(o5[1], "data sources")?, &sat)?,
GnssSystem::BeiDou => {
if is_beidou_geo(satellite_id) {
NavMessage::BeidouD2
} else {
NavMessage::BeidouD1
}
}
_ => NavMessage::GpsLnav,
}
};
let sv_accuracy_m = g(o6[0], "accuracy")?;
let sv_health = g(o6[1], "health")?;
let group_delays = match system {
GnssSystem::Gps => BroadcastGroupDelays::gps_lnav(g(o6[2], "gps tgd")?),
GnssSystem::Galileo => {
BroadcastGroupDelays::galileo(g(o6[2], "bgd e5a/e1")?, g(o6[3], "bgd e5b/e1")?)
}
GnssSystem::BeiDou => {
BroadcastGroupDelays::beidou(g(o6[2], "beidou tgd1")?, g(o6[3], "beidou tgd2")?)
}
_ => BroadcastGroupDelays::default(),
};
let fit_interval_s = match system {
GnssSystem::Gps => {
Some(gps_fit_interval_s(block[7], version).map_err(|()| bad("fit interval"))?)
}
_ => None,
};
Ok(BroadcastRecord {
satellite_id,
message,
week,
toe,
toc,
elements,
clock,
group_delays,
sv_health,
sv_accuracy_m,
fit_interval_s,
})
}
fn gps_fit_interval_s(orbit7: &str, version: RinexVersion) -> Result<f64, ()> {
let value = match field(orbit7, 23, 42) {
None => 0.0,
Some(_) => parse_f64(orbit7, 23, 42).ok_or(())?,
};
if value == 0.0 {
Ok(GPS_NOMINAL_FIT_INTERVAL_S)
} else if version.gps_fit_interval_uses_legacy_flag() && value == 1.0 {
Ok(GPS_LEGACY_EXTENDED_FIT_INTERVAL_S)
} else {
Ok(value * 3600.0)
}
}
fn galileo_message(data_sources: f64, sat: &str) -> Result<NavMessage, NavParseError> {
let word = finite_integral_u32(data_sources, "data sources", sat)?;
if word & 0b010 != 0 {
Ok(NavMessage::GalileoFnav)
} else if word & 0b101 != 0 {
Ok(NavMessage::GalileoInav)
} else {
Ok(NavMessage::GalileoInav)
}
}
fn finite_integral_u32(value: f64, field: &'static str, sat: &str) -> Result<u32, NavParseError> {
validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
if value < 0.0 || value > f64::from(u32::MAX) || value.trunc() != value {
return Err(NavParseError::BadField {
satellite: sat.to_string(),
field,
});
}
Ok(value as u32)
}
fn glonass_frequency_channel(value: f64, sat: &str) -> Result<i32, NavParseError> {
const FIELD: &str = "frequency channel";
validate::finite(value, FIELD).map_err(|error| map_record_field_error(error, sat))?;
let channel = value as i32;
if value.trunc() != value || !valid_glonass_frequency_channel(channel) {
return Err(NavParseError::BadField {
satellite: sat.to_string(),
field: FIELD,
});
}
Ok(channel)
}
fn strict_header_f64(
line: &str,
start: usize,
end: usize,
field: &'static str,
) -> Result<f64, NavParseError> {
validate::strict_f64(raw_field(line, start, end), field).map_err(map_header_field_error)
}
fn strict_header_integer_f64(
line: &str,
start: usize,
end: usize,
field: &'static str,
) -> Result<f64, NavParseError> {
let value = strict_header_f64(line, start, end, field)?;
if value.trunc() != value {
return Err(NavParseError::BadHeaderField { field });
}
Ok(value)
}
fn strict_record_int<T>(
line: &str,
start: usize,
end: usize,
field: &'static str,
satellite: &str,
) -> Result<T, NavParseError>
where
T: core::str::FromStr,
{
validate::strict_int::<T>(raw_field(line, start, end), field)
.map_err(|error| map_record_field_error(error, satellite))
}
fn map_record_field_error(error: FieldError, satellite: &str) -> NavParseError {
NavParseError::BadField {
satellite: satellite.to_string(),
field: error.field(),
}
}
fn map_header_field_error(error: FieldError) -> NavParseError {
NavParseError::BadHeaderField {
field: error.field(),
}
}
fn parse_toc(
l0: &str,
sat: &str,
time_scale: TimeScale,
) -> Result<ClockReferenceEpoch, NavParseError> {
let year = strict_record_int::<i64>(l0, 4, 8, "toc epoch", sat)?;
let month = strict_record_int::<i64>(l0, 9, 11, "toc epoch", sat)?;
let day = strict_record_int::<i64>(l0, 12, 14, "toc epoch", sat)?;
let hour = strict_record_int::<i64>(l0, 15, 17, "toc epoch", sat)?;
let minute = strict_record_int::<i64>(l0, 18, 20, "toc epoch", sat)?;
let second = strict_record_int::<i64>(l0, 21, 23, "toc epoch", sat)?;
let civil = validate::civil_datetime_with_second_policy(
year,
month,
day,
hour,
minute,
second as f64,
validate::CivilSecondPolicy::Continuous,
)
.map_err(|_| NavParseError::BadField {
satellite: sat.to_string(),
field: "toc epoch",
})?;
let month = i64::from(civil.month);
let day = i64::from(civil.day);
let week = gnss::week_from_calendar(time_scale, civil.year, month, day).ok_or_else(|| {
NavParseError::BadField {
satellite: sat.to_string(),
field: "toc epoch",
}
})?;
let sow = gnss::seconds_of_week_from_calendar(
civil.year,
month,
day,
i64::from(civil.hour),
i64::from(civil.minute),
civil.second as i64,
);
Ok(ClockReferenceEpoch { week, sow })
}
#[cfg(all(test, sidereon_repo_tests))]
mod tests;