mod store;
pub use store::{BroadcastStore, NavMessagePreference};
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, SECONDS_PER_HOUR, SECONDS_PER_WEEK};
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 * SECONDS_PER_HOUR;
pub(crate) const GLONASS_MAX_AGE_S: f64 = 15.0 * 60.0;
const GPS_NOMINAL_FIT_INTERVAL_S: f64 = 4.0 * SECONDS_PER_HOUR;
const GPS_LEGACY_EXTENDED_FIT_INTERVAL_S: f64 = 8.0 * SECONDS_PER_HOUR;
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,
GpsCnav,
GpsCnav2,
QzssCnav,
QzssCnav2,
GalileoInav,
GalileoFnav,
BeidouD1,
BeidouD2,
}
impl NavMessage {
pub const fn is_cnav_family(self) -> bool {
matches!(
self,
Self::GpsCnav | Self::GpsCnav2 | Self::QzssCnav | Self::QzssCnav2
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BroadcastIssue {
pub issue: u32,
pub message: NavMessage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BroadcastGroupDelayTerm {
GpsTgd,
GalileoBgdE5aE1,
GalileoBgdE5bE1,
BeidouTgd1,
BeidouTgd2,
CnavIscL1Ca,
CnavIscL2C,
CnavIscL5I5,
CnavIscL5Q5,
CnavIscL1Cd,
CnavIscL1Cp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CnavSignal {
L1Ca,
L2C,
L5I5,
L5Q5,
L1Cp,
L1Cd,
}
#[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>,
pub cnav_isc_l1ca_s: Option<f64>,
pub cnav_isc_l2c_s: Option<f64>,
pub cnav_isc_l5i5_s: Option<f64>,
pub cnav_isc_l5q5_s: Option<f64>,
pub cnav_isc_l1cd_s: Option<f64>,
pub cnav_isc_l1cp_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,
cnav_isc_l1ca_s: None,
cnav_isc_l2c_s: None,
cnav_isc_l5i5_s: None,
cnav_isc_l5q5_s: None,
cnav_isc_l1cd_s: None,
cnav_isc_l1cp_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,
cnav_isc_l1ca_s: None,
cnav_isc_l2c_s: None,
cnav_isc_l5i5_s: None,
cnav_isc_l5q5_s: None,
cnav_isc_l1cd_s: None,
cnav_isc_l1cp_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),
cnav_isc_l1ca_s: None,
cnav_isc_l2c_s: None,
cnav_isc_l5i5_s: None,
cnav_isc_l5q5_s: None,
cnav_isc_l1cd_s: None,
cnav_isc_l1cp_s: None,
}
}
pub const fn cnav(
tgd_s: Option<f64>,
isc_l1ca_s: Option<f64>,
isc_l2c_s: Option<f64>,
isc_l5i5_s: Option<f64>,
isc_l5q5_s: Option<f64>,
isc_l1cd_s: Option<f64>,
isc_l1cp_s: Option<f64>,
) -> Self {
Self {
gps_tgd_s: tgd_s,
galileo_bgd_e5a_e1_s: None,
galileo_bgd_e5b_e1_s: None,
beidou_tgd1_s: None,
beidou_tgd2_s: None,
cnav_isc_l1ca_s: isc_l1ca_s,
cnav_isc_l2c_s: isc_l2c_s,
cnav_isc_l5i5_s: isc_l5i5_s,
cnav_isc_l5q5_s: isc_l5q5_s,
cnav_isc_l1cd_s: isc_l1cd_s,
cnav_isc_l1cp_s: isc_l1cp_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,
BroadcastGroupDelayTerm::CnavIscL1Ca => self.cnav_isc_l1ca_s,
BroadcastGroupDelayTerm::CnavIscL2C => self.cnav_isc_l2c_s,
BroadcastGroupDelayTerm::CnavIscL5I5 => self.cnav_isc_l5i5_s,
BroadcastGroupDelayTerm::CnavIscL5Q5 => self.cnav_isc_l5q5_s,
BroadcastGroupDelayTerm::CnavIscL1Cd => self.cnav_isc_l1cd_s,
BroadcastGroupDelayTerm::CnavIscL1Cp => self.cnav_isc_l1cp_s,
}
}
pub fn cnav_single_frequency_correction_s(&self, signal: CnavSignal) -> Option<f64> {
let isc = match signal {
CnavSignal::L1Ca => self.cnav_isc_l1ca_s,
CnavSignal::L2C => self.cnav_isc_l2c_s,
CnavSignal::L5I5 => self.cnav_isc_l5i5_s,
CnavSignal::L5Q5 => self.cnav_isc_l5q5_s,
CnavSignal::L1Cp => self.cnav_isc_l1cp_s,
CnavSignal::L1Cd => self.cnav_isc_l1cd_s,
}?;
Some(self.gps_tgd_s? - isc)
}
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)
}
(
GnssSystem::Gps | GnssSystem::Qzss,
NavMessage::GpsCnav
| NavMessage::GpsCnav2
| NavMessage::QzssCnav
| NavMessage::QzssCnav2,
) => match (self.gps_tgd_s, self.cnav_isc_l1ca_s) {
(Some(tgd), Some(isc)) => Some(tgd - isc),
(Some(tgd), None) => Some(tgd),
(None, Some(isc)) => Some(-isc),
(None, None) => Some(0.0),
},
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CnavParameters {
pub adot_m_s: f64,
pub delta_n0_dot_rad_s2: f64,
pub top: GnssWeekTow,
pub ura_ed_index: i8,
pub ura_ned0_index: i8,
pub ura_ned1_index: u8,
pub ura_ned2_index: u8,
pub transmission_time_sow: f64,
pub flags: Option<u32>,
}
pub fn cnav_ura_nominal_m(index: i8) -> Option<f64> {
match index {
-16 | 15 => None,
1 => Some(2.8),
3 => Some(5.7),
5 => Some(11.3),
-15..=6 => Some(2.0_f64.powf(1.0 + f64::from(index) / 2.0)),
7..=14 => Some(2.0_f64.powi(i32::from(index) - 2)),
_ => None,
}
}
pub fn cnav_ura_ned_m(params: &CnavParameters, t: GnssWeekTow) -> Option<f64> {
let ned0 = cnav_ura_nominal_m(params.ura_ned0_index)?;
let ned1 = 2.0_f64.powi(-(14 + i32::from(params.ura_ned1_index)));
let ned2 = 2.0_f64.powi(-(28 + i32::from(params.ura_ned2_index)));
let dt_op = (f64::from(t.week) - f64::from(params.top.week)) * SECONDS_PER_WEEK
+ (t.tow_s - params.top.tow_s);
let linear = ned0 + ned1 * dt_op;
if dt_op <= 93_600.0 {
Some(linear)
} else {
Some(linear + ned2 * (dt_op - 93_600.0) * (dt_op - 93_600.0))
}
}
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 issue_of_data: BroadcastIssue,
pub week: u32,
pub toe: GnssWeekTow,
pub toc: GnssWeekTow,
pub elements: KeplerianElements,
pub clock: ClockPolynomial,
pub group_delays: BroadcastGroupDelays,
pub cnav: Option<CnavParameters>,
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,
issue_of_data: BroadcastIssue {
issue: decoded.iode as u32,
message: NavMessage::GpsLnav,
},
week: full_week,
toe,
toc,
elements,
clock,
group_delays: BroadcastGroupDelays::gps_lnav(decoded.tgd),
cnav: None,
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 * SECONDS_PER_HOUR;
const GPS_FIT_INTERVAL_8H_S: f64 = 8.0 * SECONDS_PER_HOUR;
const GPS_FIT_INTERVAL_14H_S: f64 = 14.0 * SECONDS_PER_HOUR;
const GPS_FIT_INTERVAL_26H_S: f64 = 26.0 * SECONDS_PER_HOUR;
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 {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedNavBlock {
pub satellite: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NavParse {
pub records: Vec<BroadcastRecord>,
pub skipped: Vec<SkippedNavBlock>,
}
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)
}
}
pub fn parse_nav_lenient(text: &str) -> Result<NavParse, NavParseError> {
let mut lines = text.lines();
let version = verify_and_skip_header(&mut lines)?;
let (records, skipped) = if version.major >= 4 {
parse_nav_v4_lenient(lines, version)
} else {
parse_nav_v3_lenient(lines, version)
};
Ok(NavParse { records, skipped })
}
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_v3_lenient<'a, I>(
lines: I,
version: RinexVersion,
) -> (Vec<BroadcastRecord>, Vec<SkippedNavBlock>)
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();
let mut skipped = 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) => {
match parse_keplerian_block(block, None, version) {
Ok(record) => records.push(record),
Err(error) => skipped.push(SkippedNavBlock {
satellite: nav_block_satellite(block),
message: error.to_string(),
}),
}
}
_ => {}
}
}
(records, skipped)
}
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 Some(system) = GnssSystem::from_letter(letter) else {
continue;
};
let supported = matches!(
system,
GnssSystem::Gps | GnssSystem::Galileo | GnssSystem::BeiDou | GnssSystem::Qzss
);
if !supported {
continue; }
if let Some(message) = nav_message_from_v4_token(msg_token, system) {
validate_v4_ephemeris_marker(sv, message, body)?;
if message.is_cnav_family() {
records.push(parse_cnav_block(body, message)?);
} else {
records.push(parse_keplerian_block(body, Some(message), version)?);
}
} else if known_v4_ephemeris_token(msg_token)
&& !explicitly_skipped_v4_message(msg_token, system)
{
return Err(NavParseError::BadField {
satellite: sv.to_string(),
field: "message",
});
}
}
Ok(records)
}
fn parse_nav_v4_lenient<'a, I>(
lines: I,
version: RinexVersion,
) -> (Vec<BroadcastRecord>, Vec<SkippedNavBlock>)
where
I: Iterator<Item = &'a str>,
{
let frames = v4_frames(lines);
let mut records = Vec::new();
let mut skipped = 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 Some(system) = GnssSystem::from_letter(letter) else {
continue;
};
let supported = matches!(
system,
GnssSystem::Gps | GnssSystem::Qzss | GnssSystem::Galileo | GnssSystem::BeiDou
);
if !supported {
continue;
}
if let Some(message) = nav_message_from_v4_token(msg_token, system) {
let parsed = validate_v4_ephemeris_marker(sv, message, body)
.and_then(|()| parse_keplerian_block(body, Some(message), version));
match parsed {
Ok(record) => records.push(record),
Err(error) => skipped.push(SkippedNavBlock {
satellite: sv.to_string(),
message: error.to_string(),
}),
}
}
}
(records, skipped)
}
fn nav_block_satellite(block: &[&str]) -> String {
block
.first()
.and_then(|line| line.get(0..3))
.unwrap_or("")
.trim()
.to_string()
}
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, system: GnssSystem) -> Option<NavMessage> {
match (token, system) {
("LNAV", GnssSystem::Gps) => Some(NavMessage::GpsLnav),
("CNAV", GnssSystem::Gps) => Some(NavMessage::GpsCnav),
("CNV2", GnssSystem::Gps) => Some(NavMessage::GpsCnav2),
("CNAV", GnssSystem::Qzss) => Some(NavMessage::QzssCnav),
("CNV2", GnssSystem::Qzss) => Some(NavMessage::QzssCnav2),
("INAV", GnssSystem::Galileo) => Some(NavMessage::GalileoInav),
("FNAV", GnssSystem::Galileo) => Some(NavMessage::GalileoFnav),
("D1", GnssSystem::BeiDou) => Some(NavMessage::BeidouD1),
("D2", GnssSystem::BeiDou) => Some(NavMessage::BeidouD2),
_ => None,
}
}
fn known_v4_ephemeris_token(token: &str) -> bool {
matches!(
token,
"LNAV" | "CNAV" | "CNV1" | "CNV2" | "CNV3" | "INAV" | "FNAV" | "D1" | "D2"
)
}
fn explicitly_skipped_v4_message(token: &str, system: GnssSystem) -> bool {
matches!(
(token, system),
("LNAV", GnssSystem::Qzss) | ("CNV1" | "CNV2" | "CNV3", GnssSystem::BeiDou)
)
}
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::GpsCnav | NavMessage::GpsCnav2, GnssSystem::Gps)
| (
NavMessage::QzssCnav | NavMessage::QzssCnav2,
GnssSystem::Qzss,
)
| (
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),
]
}
fn raw_orbit_field(line: &str, field_index: usize) -> &str {
const RANGES: [(usize, usize); 4] = [(4, 23), (23, 42), (42, 61), (61, 80)];
let (start, end) = RANGES[field_index];
raw_field(line, start, end)
}
#[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 issue_of_data = BroadcastIssue {
issue: finite_integral_u32(g(o1[0], "issue of data")?, "issue of data", &sat)?,
message,
};
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,
issue_of_data,
week,
toe,
toc,
elements,
clock,
group_delays,
cnav: None,
sv_health,
sv_accuracy_m,
fit_interval_s,
})
}
fn parse_cnav_block(block: &[&str], message: NavMessage) -> Result<BroadcastRecord, NavParseError> {
let l0 = block.first().copied().unwrap_or("");
let sat = l0.get(0..3).unwrap_or("").trim().to_string();
let is_cnav2 = matches!(message, NavMessage::GpsCnav2 | NavMessage::QzssCnav2);
let required_lines = if is_cnav2 { 10 } else { 9 };
if block.len() < required_lines {
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"))?;
GnssSystem::from_letter(letter).ok_or_else(|| bad("system"))?;
let satellite_id: GnssSatelliteId = sat.parse().map_err(|_| bad("prn"))?;
let toc_epoch = parse_toc(l0, &sat, TimeScale::Gpst)?;
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 o8 = orbit_row(block[8]);
let o9 = if is_cnav2 {
Some(orbit_row(block[9]))
} else {
None
};
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], "deltaN0")?,
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], "sqrtA0")?,
toe_sow: toc_epoch.sow,
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: toc_epoch.sow,
};
let week = toc_epoch.week;
let toe = GnssWeekTow::new(TimeScale::Gpst, week, elements.toe_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| bad("toe"))?;
let toc = GnssWeekTow::new(TimeScale::Gpst, week, clock.toc_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| bad("toc"))?;
let wn_op = finite_integral_u32(
g(if is_cnav2 { o9.unwrap()[1] } else { o8[1] }, "wn_op")?,
"wn_op",
&sat,
)?;
let top_sow = g(o3[0], "top")?;
let top = GnssWeekTow::new(TimeScale::Gpst, wn_op, top_sow)
.and_then(GnssWeekTow::normalized)
.map_err(|_| bad("top"))?;
let ura_ed_index = finite_integral_i8(g(o6[0], "ura_ed")?, "ura_ed", -16, 15, &sat)?;
let ura_ned0_index = finite_integral_i8(g(o5[2], "ura_ned0")?, "ura_ned0", -16, 15, &sat)?;
let ura_ned1_index = finite_integral_u8(g(o5[3], "ura_ned1")?, "ura_ned1", 0, 7, &sat)?;
let ura_ned2_index = finite_integral_u8(g(o6[3], "ura_ned2")?, "ura_ned2", 0, 7, &sat)?;
let health_max = if is_cnav2 { 1 } else { 7 };
let sv_health = f64::from(finite_integral_u8(
g(o6[1], "health")?,
"health",
0,
health_max,
&sat,
)?);
let transmission_time_sow = g(if is_cnav2 { o9.unwrap()[0] } else { o8[0] }, "t_tm")?;
let flags = optional_integral_u32(
if is_cnav2 {
raw_orbit_field(block[9], 2)
} else {
raw_orbit_field(block[8], 2)
},
"flags",
&sat,
)?;
let tgd = optional_cnav_delay(raw_orbit_field(block[6], 2), "tgd", &sat)?;
let isc_l1ca = optional_cnav_delay(raw_orbit_field(block[7], 0), "isc_l1ca", &sat)?;
let isc_l2c = optional_cnav_delay(raw_orbit_field(block[7], 1), "isc_l2c", &sat)?;
let isc_l5i5 = optional_cnav_delay(raw_orbit_field(block[7], 2), "isc_l5i5", &sat)?;
let isc_l5q5 = optional_cnav_delay(raw_orbit_field(block[7], 3), "isc_l5q5", &sat)?;
let (isc_l1cd, isc_l1cp) = if is_cnav2 {
(
optional_cnav_delay(raw_orbit_field(block[8], 0), "isc_l1cd", &sat)?,
optional_cnav_delay(raw_orbit_field(block[8], 1), "isc_l1cp", &sat)?,
)
} else {
(None, None)
};
let cnav = CnavParameters {
adot_m_s: g(o1[0], "adot")?,
delta_n0_dot_rad_s2: g(o5[1], "deltaN0Dot")?,
top,
ura_ed_index,
ura_ned0_index,
ura_ned1_index,
ura_ned2_index,
transmission_time_sow,
flags,
};
let sv_accuracy_m = cnav_ura_nominal_m(ura_ed_index).unwrap_or(8192.0);
let issue = (elements.toe_sow / 300.0).round() as u32;
Ok(BroadcastRecord {
satellite_id,
message,
issue_of_data: BroadcastIssue { issue, message },
week,
toe,
toc,
elements,
clock,
group_delays: BroadcastGroupDelays::cnav(
tgd, isc_l1ca, isc_l2c, isc_l5i5, isc_l5q5, isc_l1cd, isc_l1cp,
),
cnav: Some(cnav),
sv_health,
sv_accuracy_m,
fit_interval_s: Some(3.0 * SECONDS_PER_HOUR),
})
}
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 * SECONDS_PER_HOUR)
}
}
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 finite_integral_i8(
value: f64,
field: &'static str,
min: i8,
max: i8,
sat: &str,
) -> Result<i8, NavParseError> {
validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
if value < f64::from(min) || value > f64::from(max) || value.trunc() != value {
return Err(NavParseError::BadField {
satellite: sat.to_string(),
field,
});
}
Ok(value as i8)
}
fn finite_integral_u8(
value: f64,
field: &'static str,
min: u8,
max: u8,
sat: &str,
) -> Result<u8, NavParseError> {
validate::finite(value, field).map_err(|error| map_record_field_error(error, sat))?;
if value < f64::from(min) || value > f64::from(max) || value.trunc() != value {
return Err(NavParseError::BadField {
satellite: sat.to_string(),
field,
});
}
Ok(value as u8)
}
fn optional_integral_u32(
raw: &str,
field: &'static str,
sat: &str,
) -> Result<Option<u32>, NavParseError> {
if raw.trim().is_empty() {
return Ok(None);
}
let value =
validate::strict_f64(raw, field).map_err(|error| map_record_field_error(error, sat))?;
finite_integral_u32(value, field, sat).map(Some)
}
fn optional_cnav_delay(
raw: &str,
field: &'static str,
sat: &str,
) -> Result<Option<f64>, NavParseError> {
if raw.trim().is_empty() {
return Ok(None);
}
let value =
validate::strict_f64(raw, field).map_err(|error| map_record_field_error(error, sat))?;
if !write::d19_12_representable(value) {
return Err(NavParseError::BadField {
satellite: sat.to_string(),
field,
});
}
let mut rendered = String::new();
write::push_d19_12(&mut rendered, value);
let mut sentinel = String::new();
write::push_d19_12(&mut sentinel, -4096.0 * 2.0_f64.powi(-35));
if rendered == sentinel {
Ok(None)
} else {
Ok(Some(value))
}
}
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;