gnss_qc_traits/processing/
mask.rs

1use crate::processing::{FilterItem, ItemError};
2use thiserror::Error;
3
4/// Mask filter parsing errors
5#[derive(Error, Debug)]
6pub enum Error {
7    #[error("invalid mask item")]
8    InvalidMaskitem(#[from] ItemError),
9    #[error("missing mask operand")]
10    MissingOperand,
11    #[error("invalid mask operand")]
12    InvalidOperand,
13    #[error("invalid mask target \"{0}\"")]
14    NonSupportedTarget(String),
15    #[error("invalid mask description")]
16    InvalidDescriptor,
17}
18
19/// Masking trait, to retain specific GNSS data subsets.  
20/// This can be used to retain specific signals or [Constellation]s.
21pub trait Masking {
22    /// Apply [MaskFilter] to mutable self.
23    fn mask_mut(&mut self, mask: &MaskFilter);
24    /// Apply [MaskFilter] to immutable self.
25    fn mask(&self, mask: &MaskFilter) -> Self;
26}
27
28/// MaskOperand describes how to apply a given mask
29#[derive(Debug, Clone, PartialEq)]
30pub enum MaskOperand {
31    /// Greater than, is symbolized by ">".
32    GreaterThan,
33    /// Greater Equals, symbolized by ">=".
34    GreaterEquals,
35    /// Lower than, symbolized by "<"."
36    LowerThan,
37    /// Lower Equals, symbolized by "<=".
38    LowerEquals,
39    /// Equals, symbolized by "=".
40    /// Equals operand is implied anytime the operand is omitted in the description.
41    Equals,
42    /// Not Equals, symbolized by "!=".
43    NotEquals,
44}
45
46impl std::str::FromStr for MaskOperand {
47    type Err = Error;
48    fn from_str(content: &str) -> Result<Self, Self::Err> {
49        let c = content.trim();
50        if c.starts_with(">=") {
51            Ok(Self::GreaterEquals)
52        } else if c.starts_with('>') {
53            Ok(Self::GreaterThan)
54        } else if c.starts_with("<=") {
55            Ok(Self::LowerEquals)
56        } else if c.starts_with('<') {
57            Ok(Self::LowerThan)
58        } else if c.starts_with('=') {
59            Ok(Self::Equals)
60        } else if c.starts_with("!=") {
61            Ok(Self::NotEquals)
62        } else {
63            Err(Error::InvalidOperand)
64        }
65    }
66}
67
68impl MaskOperand {
69    pub(crate) const fn formatted_len(&self) -> usize {
70        match &self {
71            Self::Equals | Self::GreaterThan | Self::LowerThan => 1,
72            Self::NotEquals | Self::LowerEquals | Self::GreaterEquals => 2,
73        }
74    }
75}
76
77impl std::ops::Not for MaskOperand {
78    type Output = Self;
79    fn not(self) -> Self {
80        match self {
81            Self::Equals => Self::NotEquals,
82            Self::NotEquals => Self::Equals,
83            Self::GreaterEquals => Self::LowerEquals,
84            Self::GreaterThan => Self::LowerThan,
85            Self::LowerThan => Self::GreaterThan,
86            Self::LowerEquals => Self::GreaterEquals,
87        }
88    }
89}
90
91/// Apply MaskFilters to focus on datasubsets you're interested in.
92#[derive(Debug, Clone, PartialEq)]
93pub struct MaskFilter {
94    /// Item describes what subset we this [MaskFilter] applies to.
95    pub item: FilterItem,
96    /// Operand describes how to apply this [MaskFilter]
97    pub operand: MaskOperand,
98}
99
100impl std::ops::Not for MaskFilter {
101    type Output = MaskFilter;
102    fn not(self) -> Self {
103        Self {
104            operand: !self.operand,
105            item: self.item,
106        }
107    }
108}
109
110impl std::ops::BitOr for MaskFilter {
111    type Output = Self;
112    fn bitor(self, rhs: Self) -> Self {
113        if self.operand == rhs.operand {
114            Self {
115                operand: self.operand,
116                item: self.item | rhs.item,
117            }
118        } else {
119            // not permitted on operand mismatch
120            self.clone()
121        }
122    }
123}
124
125impl std::ops::BitOrAssign for MaskFilter {
126    fn bitor_assign(&mut self, rhs: Self) {
127        self.item = self.item.clone() | rhs.item;
128    }
129}
130
131impl std::str::FromStr for MaskFilter {
132    type Err = Error;
133    fn from_str(content: &str) -> Result<Self, Self::Err> {
134        let cleanedup = content.trim_start();
135        if cleanedup.len() < 2 {
136            /*
137             * we're most likely unable to parsed both
138             * an operand and a filter payload
139             */
140            return Err(Error::InvalidDescriptor);
141        }
142
143        let mut operand: Option<MaskOperand> = None;
144        let mut operand_offset: Option<usize> = None;
145        // In some cases, the target item comes first.
146        // This allows more "human readable" descriptions,
147        // but makes parsing a little harder.
148
149        // Try to locate a mask operand within given content
150        for i in 0..cleanedup.len() - 1 {
151            if i < cleanedup.len() - 2 {
152                if let Ok(op) = MaskOperand::from_str(&cleanedup[i..i + 2]) {
153                    operand = Some(op.clone());
154                    operand_offset = Some(i);
155                    break;
156                }
157            } else if let Ok(op) = MaskOperand::from_str(&cleanedup[i..i + 1]) {
158                operand = Some(op.clone());
159                operand_offset = Some(i);
160                break;
161            }
162        }
163
164        let operand_omitted = operand_offset.is_none();
165
166        let (operand, operand_offset): (MaskOperand, usize) = match operand_offset.is_some() {
167            true => (operand.unwrap(), operand_offset.unwrap()),
168            false => {
169                /*
170                 * Operand was not found, it's either omitted and Eq() is implied,
171                 * or this parser will soon fail due to faulty content
172                 */
173                (MaskOperand::Equals, 0)
174            }
175        };
176
177        if operand_offset > 0 {
178            // Some characters exist between .start() and identified operand.
179            // Type guessing for filter target will not work.
180            // This only exits for Elevation Angle, Azimuth Angle and SNR masks at the moment.
181
182            // Simply due to the fact that the operand is located
183            // after the identifier, in those cases
184
185            let start = &cleanedup[..operand_offset];
186            if start[0..1].eq("e") {
187                // --> Elevation Mask case
188                let float_offset = operand_offset + operand.formatted_len() + 2;
189                Ok(Self {
190                    operand,
191                    item: FilterItem::from_elevation(cleanedup[float_offset..].trim())?,
192                })
193            } else if content[0..1].eq("a") {
194                // --> Azimuth Mask case
195                let float_offset = operand_offset + operand.formatted_len() + 2;
196                Ok(Self {
197                    operand,
198                    item: FilterItem::from_azimuth(cleanedup[float_offset..].trim())?,
199                })
200            } else {
201                // We're only left with SNR mask case
202                let float_offset = operand_offset + operand.formatted_len() + 2;
203                if content[0..3].eq("snr") {
204                    Ok(Self {
205                        operand,
206                        item: FilterItem::from_snr(cleanedup[float_offset..].trim())?,
207                    })
208                } else {
209                    Err(Error::NonSupportedTarget(
210                        cleanedup[..operand_offset].to_string(),
211                    ))
212                }
213            }
214        } else {
215            // Descriptor starts with mask operand.
216            // Filter target type guessing is possible.
217            let offset: usize = match operand_omitted {
218                false => operand_offset + operand.formatted_len(),
219                true => 0,
220            };
221
222            Ok(Self {
223                operand,
224                item: FilterItem::from_str(cleanedup[offset..].trim_start())?,
225            })
226        }
227    }
228}
229
230#[cfg(test)]
231mod test {
232    use super::*;
233    use gnss_rs::prelude::{Constellation, SV};
234    use hifitime::Epoch;
235    use std::str::FromStr;
236    #[test]
237    fn mask_operand() {
238        for (descriptor, opposite_desc) in [
239            (">=", "<="),
240            (">", "<"),
241            ("=", "!="),
242            ("<", ">"),
243            ("<=", ">="),
244        ] {
245            let operand = MaskOperand::from_str(descriptor);
246            assert!(
247                operand.is_ok(),
248                "{} \"{}\"",
249                "Failed to parse MaskOperand from",
250                descriptor
251            );
252            let opposite = MaskOperand::from_str(opposite_desc);
253            assert!(
254                opposite.is_ok(),
255                "{} \"{}\"",
256                "Failed to parse MaskOperand from",
257                opposite_desc
258            );
259            assert_eq!(!operand.unwrap(), opposite.unwrap(), "MaskOperand::Not()");
260        }
261
262        let operand = MaskOperand::from_str("a");
263        assert!(
264            operand.is_err(),
265            "Parsed unexpectedly \"{}\" MaskOperand correctly",
266            "a"
267        );
268    }
269    #[test]
270    fn mask_epoch() {
271        let mask = MaskFilter::from_str(">2020-01-14T00:31:55 UTC").unwrap();
272        assert_eq!(
273            mask,
274            MaskFilter {
275                operand: MaskOperand::GreaterThan,
276                item: FilterItem::EpochItem(Epoch::from_str("2020-01-14T00:31:55 UTC").unwrap()),
277            }
278        );
279        let mask = MaskFilter::from_str(">JD 2452312.500372511 TAI");
280        assert!(mask.is_ok());
281    }
282    #[test]
283    fn mask_elev() {
284        for (desc, valid) in [
285            ("e>1.0", true),
286            ("e< 40.0", true),
287            ("e != 30", true),
288            (" e<40.0", true),
289            (" e < 40.0", true),
290            (" e > 120", false),
291            (" e >= 120", false),
292            (" e = 30", true),
293        ] {
294            let mask = MaskFilter::from_str(desc);
295            assert_eq!(
296                mask.is_ok(),
297                valid,
298                "failed to parse elevation mask filter \"{}\"",
299                desc
300            );
301        }
302    }
303    #[test]
304    fn mask_gnss() {
305        for (descriptor, opposite_desc) in [
306            (" = GPS", "!= GPS"),
307            ("= GAL,GPS", "!= GAL,GPS"),
308            (" =GLO,GAL", "!=  GLO,GAL"),
309        ] {
310            let mask = MaskFilter::from_str(descriptor);
311            assert!(
312                mask.is_ok(),
313                "Unable to parse MaskFilter from \"{}\"",
314                descriptor
315            );
316            let opposite = MaskFilter::from_str(opposite_desc);
317            assert!(
318                opposite.is_ok(),
319                "Unable to parse MaskFilter from \"{}\"",
320                opposite_desc
321            );
322            assert_eq!(!mask.unwrap(), opposite.unwrap(), "{}", "MaskFilter::Not()");
323        }
324
325        let mask = MaskFilter::from_str("=GPS,GAL,GLO").unwrap();
326        assert_eq!(
327            mask,
328            MaskFilter {
329                operand: MaskOperand::Equals,
330                item: FilterItem::ConstellationItem(vec![
331                    Constellation::GPS,
332                    Constellation::Galileo,
333                    Constellation::Glonass
334                ]),
335            }
336        );
337
338        let mask = MaskFilter::from_str("!=BDS").unwrap();
339        assert_eq!(
340            mask,
341            MaskFilter {
342                operand: MaskOperand::NotEquals,
343                item: FilterItem::ConstellationItem(vec![Constellation::BeiDou]),
344            }
345        );
346    }
347    #[test]
348    fn mask_sv() {
349        for (descriptor, opposite_desc) in [(" = G01", "!= G01"), ("= R03,  G31", "!= R03,  G31")] {
350            let mask = MaskFilter::from_str(descriptor);
351            assert!(
352                mask.is_ok(),
353                "Unable to parse MaskFilter from \"{}\"",
354                descriptor
355            );
356            let opposite = MaskFilter::from_str(opposite_desc);
357            assert!(
358                opposite.is_ok(),
359                "Unable to parse MaskFilter from \"{}\"",
360                opposite_desc
361            );
362            assert_eq!(!mask.unwrap(), opposite.unwrap(), "{}", "MaskFilter::Not()");
363        }
364
365        let mask = MaskFilter::from_str("=G08,  G09, R03").unwrap();
366        assert_eq!(
367            mask,
368            MaskFilter {
369                operand: MaskOperand::Equals,
370                item: FilterItem::SvItem(vec![
371                    SV::from_str("G08").unwrap(),
372                    SV::from_str("G09").unwrap(),
373                    SV::from_str("R03").unwrap(),
374                ]),
375            }
376        );
377        let m2 = MaskFilter::from_str("G08,G09,R03").unwrap();
378        assert_eq!(mask, m2);
379
380        let mask = MaskFilter::from_str("!=G31").unwrap();
381        assert_eq!(
382            mask,
383            MaskFilter {
384                operand: MaskOperand::NotEquals,
385                item: FilterItem::SvItem(vec![SV::from_str("G31").unwrap(),]),
386            }
387        );
388        let m2 = MaskFilter::from_str("!=G31").unwrap();
389        assert_eq!(mask, m2);
390    }
391    #[test]
392    fn mask_complex() {
393        let mask = MaskFilter::from_str("=L1C,S1C,D1P,C1W").unwrap();
394        assert_eq!(
395            mask,
396            MaskFilter {
397                operand: MaskOperand::Equals,
398                item: FilterItem::ComplexItem(vec![
399                    "L1C".to_string(),
400                    "S1C".to_string(),
401                    "D1P".to_string(),
402                    "C1W".to_string()
403                ])
404            }
405        );
406    }
407}