use chrono::{Datelike, NaiveDate};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sp3FileType {
PositionOnly,
PositionVelocity,
}
impl Sp3FileType {
pub fn lfn_file_type_tag(self) -> &'static str {
match self {
Self::PositionOnly => "ORB",
Self::PositionVelocity => "OBV",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sp3Version {
A,
C,
D,
}
impl Sp3Version {
pub fn lfn_version_char(self) -> char {
match self {
Self::A => 'a',
Self::C => 'c',
Self::D => 'd',
}
}
}
pub fn sample_rate_to_lfn(sample_rate_sec: u32) -> String {
if sample_rate_sec < 60 {
format!("{:02}S", sample_rate_sec)
} else if sample_rate_sec < 3600 {
format!("{:02}M", sample_rate_sec / 60)
} else {
format!("{:02}H", sample_rate_sec / 3600)
}
}
pub fn sp3_filename(
satellite: &str,
date: NaiveDate,
file_type: Sp3FileType,
sample_rate_sec: u32,
version: Sp3Version,
) -> PathBuf {
let year = date.year();
let doy = date.ordinal();
let v = version.lfn_version_char();
let smp = sample_rate_to_lfn(sample_rate_sec);
let fty = file_type.lfn_file_type_tag();
PathBuf::from(format!(
"{satellite}{v}OPSFIN_{year:04}{doy:03}0000_01D_{smp}_{fty}.SP3"
))
}
pub fn sp3_short_filename(ac: &str, date: NaiveDate, _version: Sp3Version) -> PathBuf {
let gps_epoch = NaiveDate::from_ymd_opt(1980, 1, 6).expect("GPS epoch is valid");
let days = (date - gps_epoch).num_days();
let gps_week = days / 7;
let dow = days % 7;
PathBuf::from(format!("{ac}{gps_week:04}{dow}.sp3"))
}
#[allow(clippy::too_many_arguments)]
pub fn igs_lfn(
ac: &str,
version: char,
campaign: &str,
year: i32,
doy: u32,
hour: u32,
minute: u32,
span: &str,
sample: &str,
file_type: &str,
ext: &str,
) -> PathBuf {
PathBuf::from(format!(
"{ac}{version}{campaign}_{year:04}{doy:03}{hour:02}{minute:02}_{span}_{sample}_{file_type}.{ext}",
))
}
#[allow(clippy::too_many_arguments)]
pub fn sp3_lfn(
ac: &str,
version: char,
campaign: &str,
year: i32,
doy: u32,
hour: u32,
minute: u32,
span: &str,
sample: &str,
) -> PathBuf {
igs_lfn(
ac, version, campaign, year, doy, hour, minute, span, sample, "ORB", "SP3",
)
}
#[allow(clippy::too_many_arguments)]
pub fn clk_filename(
ac: &str,
version: char,
campaign: &str,
year: i32,
doy: u32,
hour: u32,
minute: u32,
span: &str,
sample: &str,
) -> PathBuf {
igs_lfn(
ac, version, campaign, year, doy, hour, minute, span, sample, "CLK", "CLK",
)
}
#[allow(clippy::too_many_arguments)]
pub fn rnx_obs_filename(
ac: &str,
version: char,
campaign: &str,
year: i32,
doy: u32,
hour: u32,
minute: u32,
span: &str,
sample: &str,
) -> PathBuf {
igs_lfn(
ac, version, campaign, year, doy, hour, minute, span, sample, "MO", "rnx",
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn igs_lfn_format_matches_spec() {
let p = igs_lfn(
"IGS", '0', "OPSFIN", 2024, 1, 0, 0, "01D", "15M", "ORB", "SP3",
);
assert_eq!(
p.to_str().unwrap(),
"IGS0OPSFIN_20240010000_01D_15M_ORB.SP3"
);
}
#[test]
fn sp3_lfn_matches_igs_lfn() {
let a = sp3_lfn("GFZ", '0', "OPSRAP", 2024, 32, 12, 0, "01D", "05M");
let b = igs_lfn(
"GFZ", '0', "OPSRAP", 2024, 32, 12, 0, "01D", "05M", "ORB", "SP3",
);
assert_eq!(a, b);
}
#[test]
fn sp3_filename_high_level() {
let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let p = sp3_filename("IGS", date, Sp3FileType::PositionOnly, 900, Sp3Version::D);
assert_eq!(
p.to_str().unwrap(),
"IGSdOPSFIN_20240010000_01D_15M_ORB.SP3"
);
}
#[test]
fn sp3_filename_velocity_type() {
let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let p = sp3_filename(
"GFZ",
date,
Sp3FileType::PositionVelocity,
30,
Sp3Version::C,
);
let s = p.to_str().unwrap();
assert!(s.contains("OBV"), "expected OBV file-type tag, got {s}");
assert!(s.contains("30S"), "expected 30S sample rate, got {s}");
}
#[test]
fn sp3_short_filename_gps_week() {
let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let p = sp3_short_filename("igs", date, Sp3Version::D);
assert_eq!(p.to_str().unwrap(), "igs22951.sp3");
}
#[test]
fn sp3_short_filename_gps_epoch() {
let date = NaiveDate::from_ymd_opt(1980, 1, 6).unwrap();
let p = sp3_short_filename("igs", date, Sp3Version::A);
assert_eq!(p.to_str().unwrap(), "igs00000.sp3");
}
#[test]
fn clk_filename_correct_extension() {
let p = clk_filename("GFZ", '0', "OPSRAP", 2024, 1, 0, 0, "01D", "30S");
let s = p.to_str().unwrap();
assert!(s.ends_with(".CLK"), "expected .CLK, got {s}");
}
#[test]
fn rnx_obs_filename_correct_extension() {
let p = rnx_obs_filename("EUR", '0', "MGXFIN", 2024, 1, 0, 0, "01D", "30S");
let s = p.to_str().unwrap();
assert!(s.ends_with(".rnx"), "expected .rnx, got {s}");
}
#[test]
fn doy_zero_padded() {
let p = sp3_lfn("IGS", '0', "OPSFIN", 2024, 7, 0, 0, "01D", "15M");
assert!(
p.to_str().unwrap().contains("2024007"),
"DOY must be zero-padded to 3 digits"
);
}
#[test]
fn sample_rate_encoding() {
assert_eq!(sample_rate_to_lfn(5), "05S");
assert_eq!(sample_rate_to_lfn(30), "30S");
assert_eq!(sample_rate_to_lfn(60), "01M");
assert_eq!(sample_rate_to_lfn(900), "15M");
assert_eq!(sample_rate_to_lfn(3600), "01H");
}
}