1pub use crate::broadcast::{
14 eccentric_anomaly, relativistic_clock_correction_s, satellite_clock_offset_s,
15 satellite_position_ecef, satellite_position_ecef_cnav, satellite_state, satellite_state_cnav,
16 ClockOffset, ClockPolynomial, CnavRates, ConstellationConstants, EccentricAnomaly,
17 KeplerianElements, OrbitState, SatelliteState,
18};
19pub use crate::observables::{
20 is_observable_state_gap, observable_states_at_j2000_s, observable_states_at_shared_j2000_s,
21 ObservableEphemerisSource, ObservableStateBatch, ObservableStateElementStatus,
22 ObservablesError, OBSERVABLE_STATE_MISSING_POSITION_ECEF_M,
23};
24use crate::observables::{ObservableState, ObservablesInputErrorKind};
25pub use crate::orbit_determination::{
26 fit_all_sp3_ecef_precise_orbits, fit_all_sp3_precise_orbits,
27 fit_precise_ephemeris_sample_orbit, fit_precise_ephemeris_sample_orbit_with_initial_state,
28 fit_precise_ephemeris_sample_orbits, fit_precise_ephemeris_state_sample_orbit,
29 fit_precise_ephemeris_state_sample_orbits, fit_sp3_ecef_precise_orbit,
30 fit_sp3_ecef_precise_orbits, fit_sp3_precise_orbit, fit_sp3_precise_orbit_with_initial_state,
31 fit_sp3_precise_orbits, OrbitArcSpan, OrbitFitCovariance, OrbitFitError, OrbitFitOptions,
32 OrbitFitReport, OrbitFitSolution, OrbitResidualLedger, OrbitResidualStats,
33 OrientedPreciseEphemerisStateSample,
34};
35pub use crate::rinex_nav::{
36 cnav_ura_ned_m, cnav_ura_nominal_m, is_beidou_geo, BroadcastGroupDelayTerm,
37 BroadcastGroupDelays, BroadcastIssue, BroadcastRecord, CnavParameters, CnavSignal,
38 GlonassRecord, IonoCorrections, KlobucharAlphaBeta, LnavRecordError, NavMessage,
39 NavMessagePreference,
40};
41pub use crate::sp3::{
42 align_clock_reference, clock_reference_offset, compare_position_series, merge,
43 sp3_ecef_state_to_eci, AgreementMetric, ClockReferenceOffset, EpochAgreement,
44 InterpolationComparison, InterpolationDivergence, MergeCombine, MergeFlag, MergeOptions,
45 MergeReport, PreciseEphemerisInterpolant, PreciseEphemerisSample, PreciseEphemerisSamples,
46 PreciseEphemerisStateSample, PreciseInterpolantError, PreciseSamplesError, ReferenceState, Sp3,
47 Sp3DataType, Sp3Flags, Sp3Header, Sp3State, Sp3TimeSystem, Sp3Version,
48};
49pub use crate::spp::EphemerisSource;
50use crate::{validate, GnssSatelliteId, GnssSystem};
51
52pub type BroadcastEphemeris = crate::rinex_nav::BroadcastStore;
57
58#[allow(clippy::upper_case_acronyms)]
63pub type SP3 = Sp3;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum EphemerisSampleStatus {
68 Valid,
70 Gap,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct EphemerisSampleRow {
81 pub sat: GnssSatelliteId,
83 pub epoch_j2000_s: f64,
85 pub status: EphemerisSampleStatus,
87 pub position_ecef_m: Option<[f64; 3]>,
89 pub clock_s: Option<f64>,
91}
92
93impl EphemerisSampleRow {
94 pub const fn valid(
96 sat: GnssSatelliteId,
97 epoch_j2000_s: f64,
98 position_ecef_m: [f64; 3],
99 clock_s: Option<f64>,
100 ) -> Self {
101 Self {
102 sat,
103 epoch_j2000_s,
104 status: EphemerisSampleStatus::Valid,
105 position_ecef_m: Some(position_ecef_m),
106 clock_s,
107 }
108 }
109
110 pub const fn gap(sat: GnssSatelliteId, epoch_j2000_s: f64) -> Self {
112 Self {
113 sat,
114 epoch_j2000_s,
115 status: EphemerisSampleStatus::Gap,
116 position_ecef_m: None,
117 clock_s: None,
118 }
119 }
120
121 pub const fn is_gap(&self) -> bool {
123 matches!(self.status, EphemerisSampleStatus::Gap)
124 }
125}
126
127pub fn sample(
139 source: &dyn ObservableEphemerisSource,
140 satellites: &[GnssSatelliteId],
141 start_j2000_s: f64,
142 stop_j2000_s: f64,
143 step_s: f64,
144) -> core::result::Result<Vec<EphemerisSampleRow>, ObservablesError> {
145 let epochs = sample_epochs(start_j2000_s, stop_j2000_s, step_s)?;
146 let mut rows = Vec::with_capacity(satellites.len().saturating_mul(epochs.len()));
147 for &sat in satellites {
148 for &epoch_j2000_s in &epochs {
149 rows.push(sample_one(source, sat, epoch_j2000_s)?);
150 }
151 }
152 Ok(rows)
153}
154
155pub const fn broadcast_group_delay_s(
161 delays: &BroadcastGroupDelays,
162 term: BroadcastGroupDelayTerm,
163) -> Option<f64> {
164 delays.get(term)
165}
166
167pub const fn broadcast_message_group_delay_s(
173 delays: BroadcastGroupDelays,
174 system: GnssSystem,
175 message: NavMessage,
176) -> Option<f64> {
177 delays.for_message(system, message)
178}
179
180pub fn broadcast_record_group_delay_s(record: &BroadcastRecord) -> f64 {
187 record.broadcast_clock_group_delay_s()
188}
189
190fn sample_epochs(
191 start_j2000_s: f64,
192 stop_j2000_s: f64,
193 step_s: f64,
194) -> core::result::Result<Vec<f64>, ObservablesError> {
195 validate::finite(start_j2000_s, "start_j2000_s").map_err(map_sample_input_error)?;
196 validate::finite(stop_j2000_s, "stop_j2000_s").map_err(map_sample_input_error)?;
197 validate::finite_positive(step_s, "step_s").map_err(map_sample_input_error)?;
198
199 if start_j2000_s > stop_j2000_s {
200 return Ok(Vec::new());
201 }
202
203 let mut epochs = Vec::new();
204 let mut step_index = 0usize;
205 loop {
206 let epoch = start_j2000_s + step_s * step_index as f64;
207 if epoch > stop_j2000_s {
208 break;
209 }
210 if epochs.last().is_some_and(|last| epoch <= *last) {
211 return Err(sample_input_error(
212 "step_s",
213 ObservablesInputErrorKind::OutOfRange,
214 ));
215 }
216 epochs.push(epoch);
217 step_index = step_index
218 .checked_add(1)
219 .ok_or_else(|| sample_input_error("step_s", ObservablesInputErrorKind::OutOfRange))?;
220 }
221
222 if let Some(&last) = epochs.last() {
223 if last < stop_j2000_s {
224 epochs.push(stop_j2000_s);
225 }
226 }
227
228 Ok(epochs)
229}
230
231fn sample_one(
232 source: &dyn ObservableEphemerisSource,
233 sat: GnssSatelliteId,
234 epoch_j2000_s: f64,
235) -> core::result::Result<EphemerisSampleRow, ObservablesError> {
236 match source.observable_state_at_j2000_s(sat, epoch_j2000_s) {
237 Ok(state) => sample_row_from_state(sat, epoch_j2000_s, state),
238 Err(error) if is_observable_state_gap(&error) => {
239 Ok(EphemerisSampleRow::gap(sat, epoch_j2000_s))
240 }
241 Err(error) => Err(error),
242 }
243}
244
245fn sample_row_from_state(
246 sat: GnssSatelliteId,
247 epoch_j2000_s: f64,
248 state: ObservableState,
249) -> core::result::Result<EphemerisSampleRow, ObservablesError> {
250 validate::finite_vec3(state.position_ecef_m, "observable state position_ecef_m")
251 .map_err(map_sample_input_error)?;
252 if let Some(clock_s) = state.clock_s {
253 validate::finite(clock_s, "observable state clock_s").map_err(map_sample_input_error)?;
254 }
255 Ok(EphemerisSampleRow::valid(
256 sat,
257 epoch_j2000_s,
258 state.position_ecef_m,
259 state.clock_s,
260 ))
261}
262
263fn map_sample_input_error(error: validate::FieldError) -> ObservablesError {
264 sample_input_error(error.field(), ObservablesInputErrorKind::from(&error))
265}
266
267fn sample_input_error(field: &'static str, kind: ObservablesInputErrorKind) -> ObservablesError {
268 ObservablesError::InvalidInput { field, kind }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn broadcast_group_delay_free_functions_delegate_to_delay_set() {
277 let gps = BroadcastGroupDelays::gps_lnav(-2.3e-9);
278 assert_eq!(
279 broadcast_group_delay_s(&gps, BroadcastGroupDelayTerm::GpsTgd),
280 Some(-2.3e-9)
281 );
282 assert_eq!(
283 broadcast_message_group_delay_s(gps, GnssSystem::Gps, NavMessage::GpsLnav),
284 Some(-2.3e-9)
285 );
286
287 let galileo = BroadcastGroupDelays::galileo(1.0e-9, 2.0e-9);
288 assert_eq!(
289 broadcast_message_group_delay_s(galileo, GnssSystem::Galileo, NavMessage::GalileoFnav),
290 Some(1.0e-9)
291 );
292 assert_eq!(
293 broadcast_message_group_delay_s(galileo, GnssSystem::Galileo, NavMessage::GalileoInav),
294 Some(2.0e-9)
295 );
296 }
297
298 #[test]
299 fn broadcast_record_group_delay_free_function_delegates_to_record() {
300 let record = BroadcastRecord {
301 satellite_id: crate::GnssSatelliteId::new(GnssSystem::Galileo, 1)
302 .expect("valid satellite"),
303 message: NavMessage::GalileoInav,
304 issue_of_data: BroadcastIssue {
305 issue: 0,
306 message: NavMessage::GalileoInav,
307 },
308 week: 2_400,
309 toe: crate::astro::time::model::GnssWeekTow::new(
310 crate::astro::time::model::TimeScale::Gst,
311 2_400,
312 100_000.0,
313 )
314 .expect("valid toe"),
315 toc: crate::astro::time::model::GnssWeekTow::new(
316 crate::astro::time::model::TimeScale::Gst,
317 2_400,
318 100_000.0,
319 )
320 .expect("valid toc"),
321 elements: KeplerianElements {
322 sqrt_a: 5_440.0,
323 e: 0.01,
324 m0: 0.1,
325 delta_n: 0.0,
326 omega0: 0.2,
327 i0: 0.94,
328 omega: 0.3,
329 omega_dot: -8.0e-9,
330 idot: 0.0,
331 cuc: 0.0,
332 cus: 0.0,
333 crc: 0.0,
334 crs: 0.0,
335 cic: 0.0,
336 cis: 0.0,
337 toe_sow: 100_000.0,
338 },
339 clock: ClockPolynomial {
340 af0: 0.0,
341 af1: 0.0,
342 af2: 0.0,
343 toc_sow: 100_000.0,
344 },
345 group_delays: BroadcastGroupDelays::galileo(1.0e-9, 2.5e-9),
346 cnav: None,
347 sv_health: 0.0,
348 sv_accuracy_m: 1.0,
349 fit_interval_s: None,
350 };
351
352 assert_eq!(
353 broadcast_record_group_delay_s(&record).to_bits(),
354 record.broadcast_clock_group_delay_s().to_bits()
355 );
356 assert_eq!(
357 broadcast_record_group_delay_s(&record).to_bits(),
358 2.5e-9_f64.to_bits()
359 );
360 }
361}