gnss_qc/context/mod.rs
1//! GNSS processing context definition.
2use std::{
3 collections::HashMap,
4 ffi::OsStr,
5 path::{Path, PathBuf},
6};
7
8use crate::prelude::{Rinex, TimeScale};
9
10use qc_traits::Merge;
11
12pub(crate) mod blob;
13use blob::BlobData;
14
15#[cfg(feature = "flate2")]
16#[cfg_attr(docsrs, doc(cfg(feature = "flate2")))]
17mod flate2;
18
19#[cfg(feature = "sp3")]
20#[cfg_attr(docsrs, doc(cfg(feature = "sp3")))]
21mod sp3;
22
23#[cfg(feature = "navigation")]
24#[cfg_attr(docsrs, doc(cfg(feature = "navigation")))]
25mod navigation;
26
27#[cfg(feature = "navigation")]
28#[cfg_attr(docsrs, doc(cfg(feature = "navigation")))]
29pub mod time;
30
31use qc_traits::{Filter, Preprocessing, Repair, RepairTrait};
32
33use crate::{error::Error, prelude::ProductType};
34
35#[cfg(feature = "navigation")]
36use crate::prelude::{Almanac, Frame};
37
38/// [QcContext] is a general structure capable to store most common
39/// GNSS data. It is dedicated to post processing workflows,
40/// precise timing or atmosphere analysis.
41#[derive(Clone)]
42pub struct QcContext {
43 /// Files merged into this [QcContext]
44 pub(crate) files: HashMap<ProductType, Vec<PathBuf>>,
45
46 /// Context blob created by merging each members of each category
47 pub(crate) blob: HashMap<ProductType, BlobData>,
48
49 #[cfg(feature = "navigation")]
50 #[cfg_attr(docsrs, doc(cfg(feature = "navigation")))]
51 /// Latest [Almanac]
52 pub almanac: Almanac,
53
54 #[cfg(feature = "navigation")]
55 #[cfg_attr(docsrs, doc(cfg(feature = "navigation")))]
56 /// ECEF [Frame]
57 pub earth_cef: Frame,
58}
59
60impl QcContext {
61 /// Creates a new [QcContext] for GNSS post processing.
62 ///
63 /// For people interested in Post Processed navigation:
64 /// - if the library was compiled with "embed_ephem" option, you are good
65 /// to go for high precision navigation. Otherwise, this method will require
66 /// that a navigation cache is created and requires internet access on first deployment.
67 /// - for people targeting ultra high navigation precision, you should
68 /// use the JPL BPC cache and keep it up to date, by using [Self::with_jpl_update],
69 /// which requires internet access at all times.
70 ///
71 /// ```
72 /// use gnss_qc::prelude::{QcContext, TimeScale};
73 ///
74 /// // create a new (empty) context
75 /// let mut context = QcContext::new();
76 ///
77 /// // load some data
78 /// context.load_rinex_file("data/OBS/V2/AJAC3550.21O")
79 /// .unwrap();
80 ///
81 /// // do something
82 /// assert_eq!(context.timescale(), Some(TimeScale::GPST));
83 /// ```
84 pub fn new() -> Self {
85 #[cfg(feature = "navigation")]
86 let (almanac, earth_cef) = Self::default_almanac_frame();
87
88 Self {
89 files: Default::default(),
90 blob: Default::default(),
91 #[cfg(feature = "navigation")]
92 almanac,
93 #[cfg(feature = "navigation")]
94 earth_cef,
95 }
96 }
97
98 /// Returns "main" [TimeScale] for current [QcContext].
99 ///
100 /// In case measurements where provided, they will always prevail:
101 /// ```
102 /// use gnss_qc::prelude::{QcContext, TimeScale};
103 ///
104 /// // create a new (empty) context
105 /// let mut context = QcContext::new();
106 ///
107 /// // load some data
108 /// context.load_rinex_file("data/OBS/V2/AJAC3550.21O")
109 /// .unwrap();
110 ///
111 /// context.load_rinex_file("data/NAV/V2/amel0010.21g")
112 /// .unwrap();
113 ///
114 /// assert_eq!(context.timescale(), Some(TimeScale::GPST));
115 /// ```
116 ///
117 /// SP3 files have unambiguous timescale definition as well.
118 /// So they will prevail as long as RINEX measurements were not provided:
119 ///
120 /// ```
121 /// use gnss_qc::prelude::{QcContext, TimeScale};
122 ///
123 /// // create a new (empty) context
124 /// let mut context = QcContext::new();
125 ///
126 /// // load some data
127 /// context.load_gzip_sp3_file("data/SP3/D/COD0MGXFIN_20230500000_01D_05M_ORB.SP3.gz")
128 /// .unwrap();
129 ///
130 /// assert_eq!(context.timescale(), Some(TimeScale::GPST));
131 /// ```
132 pub fn timescale(&self) -> Option<TimeScale> {
133 if let Some(obs) = self.observation() {
134 let first = obs.first_epoch()?;
135 Some(first.time_scale)
136 } else if let Some(dor) = self.doris() {
137 let first = dor.first_epoch()?;
138 Some(first.time_scale)
139 } else if let Some(clk) = self.clock() {
140 let first = clk.first_epoch()?;
141 Some(first.time_scale)
142 } else if self.meteo().is_some() {
143 Some(TimeScale::UTC)
144 } else if self.ionex().is_some() {
145 Some(TimeScale::UTC)
146 } else {
147 #[cfg(feature = "sp3")]
148 if let Some(sp3) = self.sp3() {
149 return Some(sp3.header.timescale);
150 }
151
152 None
153 }
154 }
155
156 /// Returns path to File considered as Primary product in this Context.
157 /// When a unique file had been loaded, it is obviously considered Primary.
158 pub fn primary_path(&self) -> Option<&PathBuf> {
159 /*
160 * Order is important: determines what format are prioritized
161 * in the "primary" determination
162 */
163 for product in [
164 ProductType::Observation,
165 ProductType::DORIS,
166 ProductType::BroadcastNavigation,
167 ProductType::MeteoObservation,
168 ProductType::IONEX,
169 ProductType::ANTEX,
170 ProductType::HighPrecisionClock,
171 #[cfg(feature = "sp3")]
172 ProductType::HighPrecisionOrbit,
173 ] {
174 if let Some(paths) = self.files(product) {
175 /*
176 * Returns Fist file loaded in this category
177 */
178 return paths.first();
179 }
180 }
181 None
182 }
183
184 /// Returns name of this context.
185 /// Context is named after the file considered as Primary, see [Self::primary_path].
186 /// If no files were previously loaded, simply returns "Undefined".
187 pub fn name(&self) -> String {
188 if let Some(path) = self.primary_path() {
189 path.file_name()
190 .unwrap_or(OsStr::new("Undefined"))
191 .to_string_lossy()
192 // removes possible .crx ; .gz extensions
193 .split('.')
194 .next()
195 .unwrap_or("Undefined")
196 .to_string()
197 } else {
198 "Undefined".to_string()
199 }
200 }
201
202 /// Returns reference to files loaded in given category
203 pub fn files(&self, product: ProductType) -> Option<&Vec<PathBuf>> {
204 self.files
205 .iter()
206 .filter_map(|(prod_type, paths)| {
207 if *prod_type == product {
208 Some(paths)
209 } else {
210 None
211 }
212 })
213 .reduce(|k, _| k)
214 }
215
216 /// Returns mutable reference to files loaded in given category
217 pub fn files_mut(&mut self, product: ProductType) -> Option<&Vec<PathBuf>> {
218 self.files
219 .iter()
220 .filter_map(|(prod_type, paths)| {
221 if *prod_type == product {
222 Some(paths)
223 } else {
224 None
225 }
226 })
227 .reduce(|k, _| k)
228 }
229
230 /// Returns reference to inner data of given category
231 pub(crate) fn data(&self, product: ProductType) -> Option<&BlobData> {
232 self.blob
233 .iter()
234 .filter_map(|(prod_type, data)| {
235 if *prod_type == product {
236 Some(data)
237 } else {
238 None
239 }
240 })
241 .reduce(|k, _| k)
242 }
243
244 /// Returns mutable reference to inner data of given category
245 pub(crate) fn data_mut(&mut self, product: ProductType) -> Option<&mut BlobData> {
246 self.blob
247 .iter_mut()
248 .filter_map(|(prod_type, data)| {
249 if *prod_type == product {
250 Some(data)
251 } else {
252 None
253 }
254 })
255 .reduce(move |k, _| k)
256 }
257
258 /// Returns reference to inner RINEX data of given category
259 /// ```
260 /// use gnss_qc::prelude::{QcContext, ProductType};
261 ///
262 /// // create a new (empty) context
263 /// let mut context = QcContext::new();
264 ///
265 /// // load some data
266 /// context.load_rinex_file("data/OBS/V2/AJAC3550.21O")
267 /// .unwrap();
268 ///
269 /// // retrieve
270 /// let rinex = context.rinex(ProductType::Observation)
271 /// .unwrap();
272 ///
273 /// // do something
274 /// assert!(rinex.is_observation_rinex());
275 /// ```
276 pub fn rinex(&self, product: ProductType) -> Option<&Rinex> {
277 self.data(product)?.as_rinex()
278 }
279
280 /// Returns mutable reference to inner RINEX data of given category
281 pub fn rinex_mut(&mut self, product: ProductType) -> Option<&mut Rinex> {
282 self.data_mut(product)?.as_mut_rinex()
283 }
284
285 /// Returns reference to inner [ProductType::Observation] data
286 pub fn observation(&self) -> Option<&Rinex> {
287 self.data(ProductType::Observation)?.as_rinex()
288 }
289
290 /// Returns reference to inner [ProductType::DORIS] RINEX data
291 pub fn doris(&self) -> Option<&Rinex> {
292 self.data(ProductType::DORIS)?.as_rinex()
293 }
294
295 /// Returns reference to inner [ProductType::BroadcastNavigation] data
296 pub fn brdc_navigation(&self) -> Option<&Rinex> {
297 self.data(ProductType::BroadcastNavigation)?.as_rinex()
298 }
299
300 /// Returns reference to inner [ProductType::Meteo] data
301 pub fn meteo(&self) -> Option<&Rinex> {
302 self.data(ProductType::MeteoObservation)?.as_rinex()
303 }
304
305 /// Returns reference to inner [ProductType::HighPrecisionClock] data
306 pub fn clock(&self) -> Option<&Rinex> {
307 self.data(ProductType::HighPrecisionClock)?.as_rinex()
308 }
309
310 /// Returns reference to inner [ProductType::ANTEX] data
311 pub fn antex(&self) -> Option<&Rinex> {
312 self.data(ProductType::ANTEX)?.as_rinex()
313 }
314
315 /// Returns reference to inner [ProductType::IONEX] data
316 pub fn ionex(&self) -> Option<&Rinex> {
317 self.data(ProductType::IONEX)?.as_rinex()
318 }
319
320 /// Returns mutable reference to inner [ProductType::Observation] data
321 pub fn observation_mut(&mut self) -> Option<&mut Rinex> {
322 self.data_mut(ProductType::Observation)?.as_mut_rinex()
323 }
324
325 /// Returns mutable reference to inner [ProductType::DORIS] RINEX data
326 pub fn doris_mut(&mut self) -> Option<&mut Rinex> {
327 self.data_mut(ProductType::DORIS)?.as_mut_rinex()
328 }
329
330 /// Returns mutable reference to inner [ProductType::Observation] data
331 pub fn brdc_navigation_mut(&mut self) -> Option<&mut Rinex> {
332 self.data_mut(ProductType::BroadcastNavigation)?
333 .as_mut_rinex()
334 }
335
336 /// Returns reference to inner [ProductType::Meteo] data
337 pub fn meteo_mut(&mut self) -> Option<&mut Rinex> {
338 self.data_mut(ProductType::MeteoObservation)?.as_mut_rinex()
339 }
340
341 /// Returns mutable reference to inner [ProductType::HighPrecisionClock] data
342 pub fn clock_mut(&mut self) -> Option<&mut Rinex> {
343 self.data_mut(ProductType::HighPrecisionClock)?
344 .as_mut_rinex()
345 }
346
347 /// Returns mutable reference to inner [ProductType::ANTEX] data
348 pub fn antex_mut(&mut self) -> Option<&mut Rinex> {
349 self.data_mut(ProductType::ANTEX)?.as_mut_rinex()
350 }
351
352 /// Returns mutable reference to inner [ProductType::IONEX] data
353 pub fn ionex_mut(&mut self) -> Option<&mut Rinex> {
354 self.data_mut(ProductType::IONEX)?.as_mut_rinex()
355 }
356
357 /// Returns true if [ProductType::Observation] are present in Self
358 pub fn has_observation(&self) -> bool {
359 self.observation().is_some()
360 }
361
362 /// Returns true if [ProductType::BroadcastNavigation] are present in Self
363 pub fn has_brdc_navigation(&self) -> bool {
364 self.brdc_navigation().is_some()
365 }
366
367 /// Returns true if at least one [ProductType::DORIS] file is present
368 pub fn has_doris(&self) -> bool {
369 self.doris().is_some()
370 }
371
372 /// Returns true if [ProductType::MeteoObservation] are present in Self
373 pub fn has_meteo(&self) -> bool {
374 self.meteo().is_some()
375 }
376
377 /// Load a readable [Rinex] file into this [QcContext].
378 pub fn load_rinex_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
379 let rinex = Rinex::from_file(&path)?;
380 self.load_rinex(path, rinex)
381 }
382
383 /// Load a single [Rinex] file into this [QcContext].
384 /// File revision must be supported and must be correctly formatted
385 /// for this operation to be effective.
386 pub fn load_rinex<P: AsRef<Path>>(&mut self, path: P, rinex: Rinex) -> Result<(), Error> {
387 let prod_type = ProductType::from(rinex.header.rinex_type);
388
389 let path_buf = path.as_ref().to_path_buf();
390
391 // extend context blob
392 if let Some(paths) = self
393 .files
394 .iter_mut()
395 .filter_map(|(prod, files)| {
396 if *prod == prod_type {
397 Some(files)
398 } else {
399 None
400 }
401 })
402 .reduce(|k, _| k)
403 {
404 if let Some(inner) = self.blob.get_mut(&prod_type).and_then(|k| k.as_mut_rinex()) {
405 inner.merge_mut(&rinex)?;
406 paths.push(path_buf);
407 }
408 } else {
409 self.blob.insert(prod_type, BlobData::RINEX(rinex));
410 self.files.insert(prod_type, vec![path_buf]);
411 }
412
413 Ok(())
414 }
415
416 /// True if current [QcContext] is compatible with basic post processed navigation.
417 /// It does not mean you can actually perform post processed navigation, you need the "navigation"
418 /// feature for that.
419 pub fn is_navigation_compatible(&self) -> bool {
420 self.observation().is_some() && self.brdc_navigation().is_some()
421 }
422
423 /// Returns true if provided Input products allow Ionosphere bias
424 /// model optimization
425 pub fn iono_bias_model_optimization(&self) -> bool {
426 self.ionex().is_some() // TODO: BRDC V3 or V4
427 }
428
429 /// Returns true if provided Input products allow Troposphere bias
430 /// model optimization
431 pub fn tropo_bias_model_optimization(&self) -> bool {
432 self.has_meteo()
433 }
434
435 /// Apply preprocessing filter algorithm to mutable [Self].
436 /// Filter will apply to all data contained in the context.
437 pub fn filter_mut(&mut self, filter: &Filter) {
438 if let Some(data) = self.observation_mut() {
439 data.filter_mut(filter);
440 }
441 if let Some(data) = self.brdc_navigation_mut() {
442 data.filter_mut(filter);
443 }
444 if let Some(data) = self.doris_mut() {
445 data.filter_mut(filter);
446 }
447 if let Some(data) = self.meteo_mut() {
448 data.filter_mut(filter);
449 }
450 if let Some(data) = self.clock_mut() {
451 data.filter_mut(filter);
452 }
453 if let Some(data) = self.ionex_mut() {
454 data.filter_mut(filter);
455 }
456
457 #[cfg(feature = "sp3")]
458 if let Some(data) = self.sp3_mut() {
459 data.filter_mut(filter);
460 }
461 }
462
463 /// Fix given [Repair] condition
464 pub fn repair_mut(&mut self, r: Repair) {
465 if let Some(rinex) = self.observation_mut() {
466 rinex.repair_mut(r);
467 }
468 }
469
470 /// True if current [QcContext] is compatible with CPP positioning method
471 /// <https://docs.rs/gnss-rtk/latest/gnss_rtk/prelude/enum.Method.html#variant.CodePPP>.
472 /// This does not mean you can deploy a navigation solver, because that requires
473 /// the "navigation" create feature.
474 pub fn is_cpp_navigation_compatible(&self) -> bool {
475 // TODO: improve: only PR
476 if let Some(obs) = self.observation() {
477 obs.carrier_iter().count() > 1
478 } else {
479 false
480 }
481 }
482
483 /// Returns True if current [QcContext] is compatible with PPP positioning method
484 /// <https://docs.rs/gnss-rtk/latest/gnss_rtk/prelude/enum.Method.html#variant.PPP>.
485 /// This does not mean you can deploy a navigation solver, because that requires
486 /// the "navigation" create feature.
487 pub fn is_ppp_navigation_compatible(&self) -> bool {
488 // TODO: check PH as well
489 self.is_cpp_navigation_compatible()
490 }
491
492 #[cfg(not(feature = "sp3"))]
493 /// SP3 is required for 100% PPP compatibility
494 pub fn is_ppp_ultra_navigation_compatible(&self) -> bool {
495 false
496 }
497}
498
499impl std::fmt::Debug for QcContext {
500 /// Debug formatting, prints all loaded files per Product category.
501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 write!(f, "Primary: \"{}\"", self.name())?;
503 for product in [
504 ProductType::Observation,
505 ProductType::BroadcastNavigation,
506 ProductType::MeteoObservation,
507 ProductType::HighPrecisionClock,
508 ProductType::IONEX,
509 ProductType::ANTEX,
510 #[cfg(feature = "sp3")]
511 ProductType::HighPrecisionOrbit,
512 ] {
513 if let Some(files) = self.files(product) {
514 write!(f, "\n{}: ", product)?;
515 write!(f, "{:?}", files,)?;
516 }
517 }
518 Ok(())
519 }
520}