use super::*;
const SP3C_FILE: &str = "\
#cP2020 6 24 0 0 0.00000000 2 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 2 G01G02 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
++ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c G cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
%f 1.2500000 1.025000000 0.00000000000 0.000000000000000
%f 0.0000000 0.000000000 0.00000000000 0.000000000000000
%i 0 0 0 0 0 0 0 0 0
%i 0 0 0 0 0 0 0 0 0
/* TEST SP3-c FIXTURE
* 2020 6 24 0 0 0.00000000
PG01 15000.000000 -20000.000000 5000.000000 123.456789
PG02 -1234.567890 2345.678901 -3456.789012 999999.999999
* 2020 6 24 0 15 0.00000000
PG01 15100.000000 -20100.000000 5100.000000 -987.654321 E
PG02 0.000000 0.000000 0.000000 100.000000
EOF
";
const SP3D_FILE: &str = "\
#dV2022 1 2 3 4 5.00000000 1 ORBIT IGS20 FIT TST
## 2191 270245.00000000 300.00000000 59581 0.1281597222222
+ 3 G05E11C30 0 0 0 0 0 0 0 0 0 0 0 0 0 0
++ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c M cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
%f 1.2500000 1.025000000 0.00000000000 0.000000000000000
%f 0.0000000 0.000000000 0.00000000000 0.000000000000000
%i 0 0 0 0 0 0 0 0 0
%i 0 0 0 0 0 0 0 0 0
/* TEST SP3-d FIXTURE
* 2022 1 2 3 4 5.00000000
PG05 10000.000000 20000.000000 30000.000000 -50.000000
VG05 10000.000000 -20000.000000 30000.000000 1.000000
PE11 -11111.111111 22222.222222 -33333.333333 250.000000 P
VE11 -5000.000000 5000.000000 -5000.000000 2.500000
PC30 1000.000000 2000.000000 3000.000000 -10.000000 MP
VC30 1234.000000 5678.000000 9012.000000 -1.000000
EOF
";
fn id(sys: GnssSystem, prn: u8) -> GnssSatelliteId {
GnssSatelliteId::new(sys, prn)
}
#[test]
fn parses_sp3c_header() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).expect("parse SP3-c");
let h = &sp3.header;
assert_eq!(h.version, Sp3Version::C);
assert_eq!(h.data_type, Sp3DataType::Position);
assert_eq!(h.num_epochs, 2);
assert_eq!(h.coordinate_system, "IGS14");
assert_eq!(h.orbit_type, "FIT");
assert_eq!(h.agency, "TST");
assert_eq!(h.gnss_week, 2111);
assert_eq!(h.seconds_of_week, 432000.0);
assert_eq!(h.epoch_interval_s, 900.0);
assert_eq!(h.mjd, 59024);
assert_eq!(h.time_scale, TimeScale::Gpst);
assert_eq!(
h.satellites,
vec![id(GnssSystem::Gps, 1), id(GnssSystem::Gps, 2)]
);
assert_eq!(sp3.epoch_count(), 2);
assert_eq!(sp3.comments, vec!["TEST SP3-c FIXTURE".to_string()]);
}
#[test]
fn parses_sp3c_position_and_clock_units() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
let st = sp3.state(id(GnssSystem::Gps, 1), 0).unwrap();
assert_eq!(st.position.x_m, 15000.000000 * 1_000.0);
assert_eq!(st.position.y_m, -20000.000000 * 1_000.0);
assert_eq!(st.position.z_m, 5000.000000 * 1_000.0);
assert_eq!(st.clock_s, Some(123.456789 * 1.0e-6));
assert!(st.velocity.is_none());
assert!(st.clock_rate_s_s.is_none());
assert_eq!(st.flags, Sp3Flags::default());
}
#[test]
fn missing_clock_sentinel_is_none_but_position_kept() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
let st = sp3.state(id(GnssSystem::Gps, 2), 0).unwrap();
assert_eq!(st.position.x_m, -1234.567890 * 1_000.0);
assert_eq!(st.clock_s, None, "bad-clock sentinel must surface as None");
}
#[test]
fn missing_position_record_is_dropped() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
let err = sp3.state(id(GnssSystem::Gps, 2), 1).unwrap_err();
assert_eq!(err, Error::UnknownSatellite(id(GnssSystem::Gps, 2)));
assert!(sp3.state(id(GnssSystem::Gps, 1), 1).is_ok());
}
#[test]
fn clock_event_flag_parsed() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
let st = sp3.state(id(GnssSystem::Gps, 1), 1).unwrap();
assert!(st.flags.clock_event, "E flag in clock-event column");
assert!(!st.flags.orbit_predicted);
assert_eq!(st.clock_s, Some(-987.654321 * 1.0e-6));
}
#[test]
fn parses_sp3d_multignss_velocity() {
let sp3 = Sp3::parse(SP3D_FILE.as_bytes()).expect("parse SP3-d");
assert_eq!(sp3.header.version, Sp3Version::D);
assert_eq!(sp3.header.data_type, Sp3DataType::Velocity);
assert_eq!(
sp3.header.satellites,
vec![
id(GnssSystem::Gps, 5),
id(GnssSystem::Galileo, 11),
id(GnssSystem::BeiDou, 30),
]
);
let g = sp3.state(id(GnssSystem::Gps, 5), 0).unwrap();
assert_eq!(g.position.x_m, 10000.0 * 1_000.0);
let v = g.velocity.expect("velocity present");
assert_eq!(v.vx_m_s, 10000.0 * 0.1);
assert_eq!(v.vy_m_s, -20000.0 * 0.1);
assert_eq!(v.vz_m_s, 30000.0 * 0.1);
assert_ne!(v.vx_m_s, v.vy_m_s, "X and Y velocity must not be aliased");
assert_eq!(g.clock_rate_s_s, Some(1.0 * 1.0e-10));
}
#[test]
fn predicted_and_maneuver_flags_sp3d() {
let sp3 = Sp3::parse(SP3D_FILE.as_bytes()).unwrap();
let e = sp3.state(id(GnssSystem::Galileo, 11), 0).unwrap();
assert!(e.flags.orbit_predicted, "trailing P = predicted orbit");
let c = sp3.state(id(GnssSystem::BeiDou, 30), 0).unwrap();
assert!(c.flags.maneuver, "M = maneuver");
assert!(c.flags.orbit_predicted, "P after M = predicted orbit");
}
#[test]
fn epoch_index_out_of_range_errors() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
assert_eq!(
sp3.state(id(GnssSystem::Gps, 1), 99),
Err(Error::EpochOutOfRange)
);
assert!(sp3.states_at(99).is_err());
}
#[test]
fn epoch_julian_split_is_consistent() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
let e0 = sp3.epochs[0].julian_date().unwrap();
let e1 = sp3.epochs[1].julian_date().unwrap();
assert_eq!(e0.jd_whole, e1.jd_whole, "same civil day");
assert_eq!(e1.fraction - e0.fraction, 900.0 / 86_400.0);
assert_eq!(e0.jd_whole + e0.fraction, 2_459_024.5);
}
#[test]
fn missing_header_line1_errors() {
let no_h1 = "\
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
EOF
";
let err = Sp3::parse(no_h1.as_bytes()).unwrap_err();
assert!(matches!(err, Error::Parse(_)));
}
#[test]
fn missing_header_line2_errors() {
let no_h2 = "\
#cP2020 6 24 0 0 0.00000000 2 ORBIT IGS14 FIT TST
EOF
";
let err = Sp3::parse(no_h2.as_bytes()).unwrap_err();
assert!(matches!(err, Error::Parse(_)));
}
#[test]
fn unknown_time_system_errors() {
let glo = "\
#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 R01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c R cc GLO ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
EOF
";
let err = Sp3::parse(glo.as_bytes()).unwrap_err();
assert!(
matches!(err, Error::Parse(ref m) if m.contains("GLO")),
"got {err:?}"
);
}
#[test]
fn missing_pc_descriptor_errors_for_sp3c() {
let no_pc = "\
#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 G01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
* 2020 6 24 0 0 0.00000000
PG01 15000.000000 -20000.000000 5000.000000 123.456789
EOF
";
let err = Sp3::parse(no_pc.as_bytes()).unwrap_err();
assert!(
matches!(err, Error::Parse(ref m) if m.contains("time system")),
"missing %c must error, not default to GPST; got {err:?}"
);
}
#[test]
fn short_pc_descriptor_errors_for_sp3c() {
let short_pc = "\
#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 G01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c G
EOF
";
let err = Sp3::parse(short_pc.as_bytes()).unwrap_err();
assert!(
matches!(err, Error::Parse(ref m) if m.contains("too short")),
"short %c must error; got {err:?}"
);
}
#[test]
fn blank_pc_time_system_errors_for_sp3c() {
let blank_pc = "\
#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 G01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c G cc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
EOF
";
let err = Sp3::parse(blank_pc.as_bytes()).unwrap_err();
assert!(
matches!(err, Error::Parse(ref m) if m.contains("blank")),
"blank %c time system must error; got {err:?}"
);
}
#[test]
fn sp3a_with_no_pc_descriptor_is_gpst() {
let sp3a = "\
#aP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
* 2020 6 24 0 0 0.00000000
P 1 15000.000000 -20000.000000 5000.000000 123.456789
EOF
";
let sp3 = Sp3::parse(sp3a.as_bytes()).expect("SP3-a parses without %c");
assert_eq!(sp3.header.version, Sp3Version::A);
assert_eq!(sp3.header.time_scale, TimeScale::Gpst);
assert!(sp3.state(id(GnssSystem::Gps, 1), 0).is_ok());
}
#[test]
fn sp3a_ignores_pc_descriptor_and_stays_gpst() {
let sp3a = "\
#aP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c R cc GLO ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
* 2020 6 24 0 0 0.00000000
P 1 15000.000000 -20000.000000 5000.000000 123.456789
EOF
";
let sp3 = Sp3::parse(sp3a.as_bytes()).expect("SP3-a parses, ignoring %c");
assert_eq!(sp3.header.time_scale, TimeScale::Gpst);
}
#[test]
fn valid_gps_pc_descriptor_parses() {
let sp3 = Sp3::parse(SP3C_FILE.as_bytes()).unwrap();
assert_eq!(sp3.header.time_scale, TimeScale::Gpst);
}
#[test]
fn velocity_only_record_produces_no_state() {
let vel_only = "\
#dV2022 1 2 3 4 5.00000000 1 ORBIT IGS20 FIT TST
## 2191 270245.00000000 300.00000000 59581 0.1281597222222
+ 1 G05 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c M cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
* 2022 1 2 3 4 5.00000000
VG05 10000.000000 -20000.000000 30000.000000 1.000000
EOF
";
let sp3 = Sp3::parse(vel_only.as_bytes()).expect("parse velocity-only");
let err = sp3.state(id(GnssSystem::Gps, 5), 0).unwrap_err();
assert_eq!(err, Error::UnknownSatellite(id(GnssSystem::Gps, 5)));
assert!(
sp3.states_at(0).unwrap().is_empty(),
"no (0,0,0) state leaked"
);
}
#[test]
fn position_then_velocity_augments_velocity() {
let sp3 = Sp3::parse(SP3D_FILE.as_bytes()).unwrap();
let st = sp3.state(id(GnssSystem::Gps, 5), 0).unwrap();
assert_eq!(st.position.x_m, 10000.0 * 1_000.0);
let v = st
.velocity
.expect("velocity augmented onto the P-record state");
assert_eq!(v.vx_m_s, 10000.0 * 0.1);
assert_eq!(v.vy_m_s, -20000.0 * 0.1);
assert_eq!(v.vz_m_s, 30000.0 * 0.1);
}
#[test]
fn position_record_before_epoch_errors() {
let bad = "\
#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
+ 1 G01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
%c G cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
PG01 15000.000000 -20000.000000 5000.000000 123.456789
EOF
";
let err = Sp3::parse(bad.as_bytes()).unwrap_err();
assert!(
matches!(err, Error::Parse(ref m) if m.contains("before any epoch")),
"got {err:?}"
);
}
#[test]
fn non_utf8_input_errors() {
let bytes = [0xffu8, 0xfe, 0x00, 0x01];
let err = Sp3::parse(&bytes).unwrap_err();
assert!(
matches!(err, Error::Parse(ref m) if m.contains("UTF-8")),
"got {err:?}"
);
}
#[test]
fn trailing_truncation_after_eof_tolerated() {
let truncated = format!("{SP3C_FILE}garbage line that should be ignored\n");
let sp3 = Sp3::parse(truncated.as_bytes()).unwrap();
assert_eq!(sp3.epoch_count(), 2);
}
#[test]
fn sv_token_round_trips_through_display() {
for sys in [
GnssSystem::Gps,
GnssSystem::Glonass,
GnssSystem::Galileo,
GnssSystem::BeiDou,
GnssSystem::Qzss,
GnssSystem::Navic,
GnssSystem::Sbas,
] {
for prn in [1u8, 5, 9, 12, 30, 99] {
let want = id(sys, prn);
let token = want.to_string(); let got = super::parse_sv_token(&token, Some(Sp3Version::D))
.unwrap_or_else(|| panic!("token {token:?} failed to parse"));
assert_eq!(got, want);
}
}
}
#[test]
fn sp3a_bare_numeric_prn_is_gps() {
assert_eq!(
super::parse_sv_token(" 7", Some(Sp3Version::A)),
Some(id(GnssSystem::Gps, 7))
);
assert_eq!(
super::parse_sv_token("23", Some(Sp3Version::A)),
Some(id(GnssSystem::Gps, 23))
);
}
#[test]
fn multibyte_line_does_not_panic() {
let file = "\
#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST
## 2111 432000.00000000 900.00000000 59024 0.0000000000000
%c G cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
* 2020 6 24 0 0 0.00000000
PG01 15000.000000 -20000.000000 5000.000000 123.456789 \u{00e9}\u{00e9}\u{00e9}
EOF
";
let _ = Sp3::parse(file.as_bytes());
}
#[test]
fn coordinate_sign_and_magnitude_preserved() {
for &km in &[0.000001f64, -12345.678901, 26560.123456, -26560.999999] {
let line = format!("PG01{:14.6}{:14.6}{:14.6}{:14.6}", km, km, km, 0.0);
let file = format!(
"#cP2020 6 24 0 0 0.00000000 1 ORBIT IGS14 FIT TST\n\
## 2111 432000.00000000 900.00000000 59024 0.0000000000000\n\
+ 1 G01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n\
%c G cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n\
* 2020 6 24 0 0 0.00000000\n\
{line}\n\
EOF\n"
);
let sp3 = Sp3::parse(file.as_bytes()).unwrap();
let st = sp3.state(id(GnssSystem::Gps, 1), 0).unwrap();
assert_eq!(st.position.x_m, km * 1_000.0, "km={km}");
assert_eq!(st.position.z_m, km * 1_000.0, "km={km}");
}
}