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}