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
8extern 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
65pub type Comments = Vec<String>;
67
68pub mod prelude {
69 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)]
111pub struct DORIS {
115 pub header: Header,
117
118 pub record: Record,
120
121 pub production: Option<ProductionAttributes>,
124}
125
126impl DORIS {
127 pub fn new(header: Header, record: Record) -> DORIS {
129 DORIS {
130 header,
131 record,
132 production: Default::default(),
133 }
134 }
135
136 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 pub fn replace_header(&mut self, header: Header) {
147 self.header = header.clone();
148 }
149
150 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 pub fn replace_record(&mut self, record: Record) {
161 self.record = record.clone();
162 }
163
164 pub fn parse<R: Read>(reader: &mut BufReader<R>) -> Result<Self, ParsingError> {
166 let mut header = Header::parse(reader)?;
168
169 let record = Record::parse(&mut header, reader)?;
172
173 Ok(Self {
174 header,
175 record,
176 production: Default::default(),
177 })
178 }
179
180 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 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<DORIS, ParsingError> {
191 let path = path.as_ref();
192
193 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 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 #[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 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 #[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 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 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 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 pub fn dominant_sampling_period(&self) -> Option<Duration> {
313 None }
315
316 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 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 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}