doris_rs/
lib.rs

1#![doc(
2    html_logo_url = "https://raw.githubusercontent.com/nav-solutions/.github/master/logos/logo2.jpg"
3)]
4#![doc = include_str!("../README.md")]
5#![cfg_attr(docsrs, feature(doc_cfg))]
6#![allow(clippy::type_complexity)]
7
8/*
9 * DORIS is part of the nav-solutions framework.
10 * Authors: Guillaume W. Bres <guillaume.bressaix@gmail.com> et al.
11 * (cf. https://github.com/nav-solutions/doris/graphs/contributors)
12 * This framework is licensed under Mozilla Public V2 license.
13 *
14 * Documentation: https://github.com/nav-solutions/doris
15 */
16
17extern crate num_derive;
18
19#[cfg(feature = "serde")]
20#[macro_use]
21extern crate serde;
22
23extern crate gnss_rs as gnss;
24extern crate num;
25
26pub mod constants;
27pub mod error;
28pub mod frequency;
29pub mod header;
30pub mod matcher;
31pub mod observable;
32pub mod production;
33pub mod record;
34pub mod station;
35
36mod epoch;
37
38#[cfg(test)]
39mod tests;
40
41use std::{
42    fs::File,
43    io::{BufReader, BufWriter, Read, Write},
44    path::Path,
45    str::FromStr,
46};
47
48use itertools::Itertools;
49
50#[cfg(feature = "flate2")]
51use flate2::{read::GzDecoder, write::GzEncoder, Compression as GzCompression};
52
53use hifitime::prelude::{Duration, Epoch};
54
55use crate::{
56    error::{Error, FormattingError, ParsingError},
57    header::Header,
58    matcher::Matcher,
59    observable::Observable,
60    production::ProductionAttributes,
61    record::{ClockOffset, Record},
62    station::GroundStation,
63};
64
65/// [Comments] found in [DORIS] files
66pub type Comments = Vec<String>;
67
68pub mod prelude {
69    // export
70    pub use crate::{
71        error::{FormattingError, ParsingError},
72        frequency::Frequency,
73        header::{Antenna, Header, Receiver, Version},
74        matcher::Matcher,
75        observable::Observable,
76        production::ProductionAttributes,
77        record::{ClockOffset, EpochFlag, Key, Measurements, Observation, Record, SNR},
78        station::GroundStation,
79        Comments, DORIS,
80    };
81
82    pub use gnss::prelude::{Constellation, DOMESTrackingPoint, COSPAR, DOMES, SV};
83
84    pub use hifitime::{Duration, Epoch, Polynomial, TimeScale, TimeSeries};
85}
86
87pub(crate) fn fmt_doris(content: &str, marker: &str) -> String {
88    if content.len() < 60 {
89        format!("{:<padding$}{}", content, marker, padding = 60)
90    } else {
91        let mut string = String::new();
92        let nb_lines = num_integer::div_ceil(content.len(), 60);
93        for i in 0..nb_lines {
94            let start_off = i * 60;
95            let end_off = std::cmp::min(start_off + 60, content.len());
96            let chunk = &content[start_off..end_off];
97            string.push_str(&format!("{:<padding$}{}", chunk, marker, padding = 60));
98            if i < nb_lines - 1 {
99                string.push('\n');
100            }
101        }
102        string
103    }
104}
105
106pub(crate) fn fmt_comment(content: &str) -> String {
107    fmt_doris(content, "COMMENT")
108}
109
110#[derive(Clone, Default, Debug, PartialEq)]
111/// [DORIS] is composed of a [Header] and a [Record] section.
112/// ```
113/// ```
114pub struct DORIS {
115    /// [Header] gives general information
116    pub header: Header,
117
118    /// [Record] gives the actual file content
119    pub record: Record,
120
121    /// [ProductionAttributes] is attached to files that were
122    /// named according to the standard conventions.
123    pub production: Option<ProductionAttributes>,
124}
125
126impl DORIS {
127    /// Builds a new [DORIS] struct from given [Header] and [Record] sections.
128    pub fn new(header: Header, record: Record) -> DORIS {
129        DORIS {
130            header,
131            record,
132            production: Default::default(),
133        }
134    }
135
136    /// Copy and return this [DORIS] with updated [Header].
137    pub fn with_header(&self, header: Header) -> Self {
138        Self {
139            header,
140            record: self.record.clone(),
141            production: Default::default(),
142        }
143    }
144
145    /// Replace [Header] with mutable access.
146    pub fn replace_header(&mut self, header: Header) {
147        self.header = header.clone();
148    }
149
150    /// Copies and returns a [DORIS] with updated [Record]
151    pub fn with_record(&self, record: Record) -> Self {
152        DORIS {
153            record,
154            header: self.header.clone(),
155            production: self.production.clone(),
156        }
157    }
158
159    /// Replace [Record] with mutable access.
160    pub fn replace_record(&mut self, record: Record) {
161        self.record = record.clone();
162    }
163
164    /// Parse [DORIS] content by consuming [BufReader] (efficient buffered reader).
165    pub fn parse<R: Read>(reader: &mut BufReader<R>) -> Result<Self, ParsingError> {
166        // Parses Header section (=consumes header until this point)
167        let mut header = Header::parse(reader)?;
168
169        // Parse record (=consumes rest of this resource)
170        // Comments are preserved and store "as is"
171        let record = Record::parse(&mut header, reader)?;
172
173        Ok(Self {
174            header,
175            record,
176            production: Default::default(),
177        })
178    }
179
180    /// Format [DORIS] into writable I/O using efficient buffered writer
181    /// and following standard specifications. This is the mirror operation of [Self::parse].
182    pub fn format<W: Write>(&self, writer: &mut BufWriter<W>) -> Result<(), FormattingError> {
183        self.header.format(writer)?;
184        self.record.format(writer, &self.header)?;
185        writer.flush()?;
186        Ok(())
187    }
188
189    /// Parses [DORIS] from local readable file.
190    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<DORIS, ParsingError> {
191        let path = path.as_ref();
192
193        // deduce all we can from file name
194        let file_attributes = match path.file_name() {
195            Some(filename) => {
196                let filename = filename.to_string_lossy().to_string();
197                if let Ok(prod) = ProductionAttributes::from_str(&filename) {
198                    prod
199                } else {
200                    ProductionAttributes::default()
201                }
202            },
203            _ => ProductionAttributes::default(),
204        };
205
206        let fd = File::open(path).expect("from_file: open error");
207
208        let mut reader = BufReader::new(fd);
209        let mut doris = Self::parse(&mut reader)?;
210
211        doris.production = Some(file_attributes);
212
213        Ok(doris)
214    }
215
216    /// Dumps [DORIS] into writable local file (as readable ASCII UTF-8)
217    /// using efficient buffered formatting.
218    /// This is the mirror operation of [Self::from_file].
219    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), FormattingError> {
220        let fd = File::create(path)?;
221        let mut writer = BufWriter::new(fd);
222        self.format(&mut writer)?;
223        Ok(())
224    }
225
226    /// Parses [DORIS] from local gzip compressed file.
227    #[cfg(feature = "flate2")]
228    #[cfg_attr(docsrs, doc(cfg(feature = "flate2")))]
229    pub fn from_gzip_file<P: AsRef<Path>>(path: P) -> Result<DORIS, ParsingError> {
230        let path = path.as_ref();
231
232        // deduce all we can from file name
233        let file_attributes = match path.file_name() {
234            Some(filename) => {
235                let filename = filename.to_string_lossy().to_string();
236                if let Ok(prod) = ProductionAttributes::from_str(&filename) {
237                    prod
238                } else {
239                    ProductionAttributes::default()
240                }
241            },
242            _ => ProductionAttributes::default(),
243        };
244
245        let fd = File::open(path).expect("from_file: open error");
246
247        let reader = GzDecoder::new(fd);
248        let mut reader = BufReader::new(reader);
249        let mut doris = Self::parse(&mut reader)?;
250
251        doris.production = Some(file_attributes);
252
253        Ok(doris)
254    }
255
256    /// Dumps and gzip encodes [DORIS] into writable local file,
257    /// using efficient buffered formatting.
258    /// This is the mirror operation of [Self::from_gzip_file].
259    #[cfg(feature = "flate2")]
260    #[cfg_attr(docsrs, doc(cfg(feature = "flate2")))]
261    pub fn to_gzip_file<P: AsRef<Path>>(&self, path: P) -> Result<(), FormattingError> {
262        let fd = File::create(path)?;
263        let compression = GzCompression::new(5);
264        let mut writer = BufWriter::new(GzEncoder::new(fd, compression));
265        self.format(&mut writer)?;
266        Ok(())
267    }
268
269    /// Determines whether this structure results of combining several structures
270    /// into a single one. This is determined by the presence of a custom yet somewhat standardized Header comment.
271    pub fn is_merged(&self) -> bool {
272        let special_comment = String::from("FILE MERGE");
273        for comment in self.header.comments.iter() {
274            if comment.eq("FILE MERGE") {
275                return true;
276            }
277        }
278        false
279    }
280
281    /// Returns [GroundStation] information for matching site
282    pub fn ground_station<'a>(&self, matcher: Matcher<'a>) -> Option<GroundStation> {
283        self.header
284            .ground_stations
285            .iter()
286            .filter(|station| station.matches(&matcher))
287            .reduce(|k, _| k)
288            .cloned()
289    }
290
291    /// Returns measurement satellite [ClockOffset] [Iterator] for all Epochs, in chronological order
292    pub fn satellite_clock_offset_iter(
293        &self,
294    ) -> Box<dyn Iterator<Item = (Epoch, ClockOffset)> + '_> {
295        Box::new(
296            self.record
297                .measurements
298                .iter()
299                .filter_map(|(k, v)| {
300                    if let Some(clock_offset) = v.satellite_clock_offset {
301                        Some((k.epoch, clock_offset))
302                    } else {
303                        None
304                    }
305                })
306                .unique(),
307        )
308    }
309
310    /// Studies actual measurement rate and returns the highest
311    /// value in the histogram as the dominant sampling rate
312    pub fn dominant_sampling_period(&self) -> Option<Duration> {
313        None // TODO
314    }
315
316    /// Copies and returns new [DORIS] that is the result
317    /// of ground station observation differentiation.
318    /// See [Self::observations_substract_mut] for more information.
319    pub fn substract(&self, rhs: &Self) -> Result<Self, Error> {
320        let mut s = self.clone();
321        s.substract_mut(rhs)?;
322        Ok(s)
323    }
324
325    /// TODO
326    pub fn substract_mut(&mut self, rhs: &Self) -> Result<(), Error> {
327        let lhs_dt = self
328            .dominant_sampling_period()
329            .ok_or(Error::UndeterminedSamplingRate)?;
330
331        let half_lhs_dt = lhs_dt / 2.0;
332
333        // if let Some(rhs) = rhs.record.as_obs() {
334        //     if let Some(rec) = self.record.as_mut_obs() {
335        //         rec.retain(|k, v| {
336        //             v.signals.retain_mut(|sig| {
337        //                 let mut reference = 0.0;
338        //                 let mut min_dt = Duration::MAX;
339
340        //                 // temporal filter
341        //                 let filtered_rhs_epochs = rhs.iter().filter(|(rhs, _)| {
342        //                     let dt = (rhs.epoch - k.epoch).abs();
343        //                     dt <= half_lhs_dt
344        //                 });
345
346        //                 for (rhs_epoch, rhs_values) in filtered_rhs_epochs {
347        //                     for rhs_sig in rhs_values.signals.iter() {
348        //                         if rhs_sig.sv == sig.sv && rhs_sig.observable == sig.observable {
349        //                             let dt = (rhs_epoch.epoch - k.epoch).abs();
350        //                             if dt <= min_dt {
351        //                                 reference = rhs_sig.value;
352        //                                 min_dt = dt;
353        //                             }
354        //                         }
355        //                     }
356        //                 }
357
358        //                 if min_dt < Duration::MAX {
359        //                     sig.value -= reference;
360        //                 }
361
362        //                 min_dt < Duration::MAX
363        //             });
364
365        //             !v.signals.is_empty()
366        //         });
367        //     }
368        // }
369
370        Ok(())
371    }
372}
373
374#[cfg(test)]
375mod test {
376    use super::*;
377    use crate::fmt_comment;
378
379    #[test]
380    fn fmt_comments_singleline() {
381        for desc in [
382            "test",
383            "just a basic comment",
384            "just another lengthy comment blahblabblah",
385        ] {
386            let comment = fmt_comment(desc);
387            assert!(
388                comment.len() >= 60,
389                "comments should be at least 60 byte long"
390            );
391
392            assert_eq!(
393                comment.find("COMMENT"),
394                Some(60),
395                "comment marker should located @ 60"
396            );
397        }
398    }
399
400    #[test]
401    fn fmt_wrapped_comments() {
402        for desc in ["just trying to form a very lengthy comment that will overflow since it does not fit in a single line",
403            "just trying to form a very very lengthy comment that will overflow since it does fit on three very meaningful lines. Imazdmazdpoakzdpoakzpdokpokddddddddddddddddddaaaaaaaaaaaaaaaaaaaaaaa"] {
404            let nb_lines = num_integer::div_ceil(desc.len(), 60);
405            let comments = fmt_comment(desc);
406            assert_eq!(comments.lines().count(), nb_lines);
407            for line in comments.lines() {
408                assert!(line.len() >= 60, "comment line should be at least 60 byte long");
409                assert_eq!(line.find("COMMENT"), Some(60), "comment marker should located @ 60");
410            }
411        }
412    }
413
414    #[test]
415    fn fmt_observables_v3() {
416        for (desc, expected) in [
417("R    9 C1C L1C S1C C2C C2P L2C L2P S2C S2P",
418"R    9 C1C L1C S1C C2C C2P L2C L2P S2C S2P                  SYS / # / OBS TYPES"),
419("G   18 C1C L1C S1C C2P C2W C2S C2L C2X L2P L2W L2S L2L L2X         S2P S2W S2S S2L S2X",
420"G   18 C1C L1C S1C C2P C2W C2S C2L C2X L2P L2W L2S L2L L2X  SYS / # / OBS TYPES
421       S2P S2W S2S S2L S2X                                  SYS / # / OBS TYPES"),
422        ] {
423            assert_eq!(fmt_doris(desc, "SYS / # / OBS TYPES"), expected);
424        }
425    }
426}