eo_identifiers/identifiers/
sentinel1.rs

1//! Sentinel 1
2//!
3//! # Example
4//!
5//! ```rust
6//! use eo_identifiers::identifiers::sentinel1::{Product, Dataset};
7//! use std::str::FromStr;
8//!
9//! assert!(
10//!     Product::from_str("S1A_EW_GRDM_1SDH_20151221T165227_20151221T165332_009143_00D275_8694")
11//!     .is_ok()
12//! );
13//! assert!(
14//!     Dataset::from_str("s1a-iw-grd-vh-20221029t171425-20221029t171450-045660-0575ce-002")
15//!     .is_ok()
16//! );
17//! ```
18//!
19use crate::common_parsers::{parse_esa_timestamp, take_n_digits_in_range};
20use crate::{impl_from_str, Mission};
21use chrono::NaiveDateTime;
22use nom::branch::alt;
23use nom::bytes::complete::{tag, tag_no_case, take_while_m_n};
24use nom::character::complete::char;
25use nom::combinator::map;
26use nom::IResult;
27#[cfg(feature = "serde")]
28use serde::{Deserialize, Serialize};
29
30#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32pub enum MissionId {
33    S1A,
34    S1B,
35}
36
37impl From<MissionId> for Mission {
38    fn from(_: MissionId) -> Self {
39        Mission::Sentinel1
40    }
41}
42
43#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub enum Mode {
46    IW,
47    EW,
48    WV,
49    S1,
50    S2,
51    S3,
52    S4,
53    S5,
54    S6,
55}
56
57#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59pub enum ProductType {
60    RAW,
61    SLC,
62    GRD,
63    OCN,
64}
65
66#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68pub enum ResolutionClass {
69    Full,
70    High,
71    Medium,
72    NotApplicable,
73}
74
75#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77pub enum ProcessingLevel {
78    Level0,
79    Level1,
80    Level2,
81}
82
83#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
84#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
85pub enum ProductClass {
86    Standard,
87    Annotation,
88}
89
90#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92pub enum ProductPolarisation {
93    HH,
94    VV,
95    HHHV,
96    VVVH,
97}
98
99/// Sentinel 1 Product
100///
101/// Based on the [official S1 naming convention](https://sentinel.esa.int/web/sentinel/user-guides/sentinel-1-sar/naming-conventions).
102#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
103#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
104pub struct Product {
105    /// Mission id
106    ///
107    /// # Official description
108    ///
109    /// > The Mission Identifier (MMM) denotes the satellite and will be either
110    /// > S1A for the SENTINEL-1A instrument or S1B for the SENTINEL-1B instrument.
111    pub mission_id: MissionId,
112
113    /// Mode
114    ///
115    /// # Official description
116    ///
117    /// > The Mode/Beam (BB) identifies the S1-S6 beams for SM products and IW, EW and WV for
118    /// > products from the respective modes.
119    pub mode: Mode,
120
121    /// Product Type
122    ///
123    /// # Official description
124    ///
125    /// > Product Type (TTT) can be RAW, SLC, GRD or OCN.
126    pub product_type: ProductType,
127
128    /// Resolution class
129    ///
130    /// # Official description
131    ///
132    /// > Resolution Class (R) can be F (Full resolution), H (High resolution),
133    /// > M (Medium resolution), or _ (underscore: not applicable to the current product type). Resolution Class is used for GRD only.
134    pub resolution_class: ResolutionClass,
135
136    /// Processing level
137    ///
138    /// # Official description
139    ///
140    /// > The Processing Level (L) can be 0, 1 or 2.
141    pub processing_level: ProcessingLevel,
142
143    /// Product class
144    ///
145    /// # Official description
146    ///
147    /// > The Product Class can be Standard (S) or Annotation (A). Annotation products are
148    /// > only used internally by the PDGS and are not distributed.
149    pub product_class: ProductClass,
150
151    /// Polarisation
152    ///
153    /// # Official description
154    ///
155    /// > Polarisation (PP) can be one of:
156    /// > * SH (single HH polarisation)
157    /// > * SV (single VV polarisation)
158    /// > * DH (dual HH+HV polarisation)
159    /// > * DV (dual VV+VH polarisation)
160    pub polarisation: ProductPolarisation,
161
162    /// start datetime
163    pub start_datetime: NaiveDateTime,
164
165    /// stop datetime
166    pub stop_datetime: NaiveDateTime,
167
168    /// Orbit number
169    ///
170    /// # Official description
171    ///
172    /// > The absolute orbit number at product start time (OOOOOO) will be
173    /// > in the range 000001-999999.
174    pub orbit_number: u32,
175
176    /// Data take identifier
177    ///
178    /// # Official description
179    ///
180    /// > The mission data-take identifier (DDDDDD) will be in the range 000001-FFFFFF.
181    pub data_take_identifier: String,
182
183    /// product unique identifier
184    ///
185    /// # Official description
186    ///
187    /// > The product unique identifier (CCCC) is a hexadecimal string generated by
188    /// > computing CRC-16 on the manifest file using CRC-CCITT.
189    pub product_unique_identifier: String,
190    // folder extension is skipped
191}
192
193#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash, Copy)]
194#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
195pub enum SwathIdentifier {
196    S1,
197    S2,
198    S3,
199    S4,
200    S5,
201    S6,
202    IW,
203    IW1,
204    IW2,
205    IW3,
206    EW,
207    EW1,
208    EW2,
209    EW3,
210    EW4,
211    EW5,
212    WV,
213    WV1,
214    WV2,
215}
216
217impl SwathIdentifier {
218    pub fn is_s(&self) -> bool {
219        matches!(
220            self,
221            Self::S1 | Self::S2 | Self::S3 | Self::S4 | Self::S5 | Self::S6
222        )
223    }
224
225    pub fn is_iw(&self) -> bool {
226        matches!(self, Self::IW1 | Self::IW2 | Self::IW3 | Self::IW)
227    }
228
229    pub fn is_ew(&self) -> bool {
230        matches!(self, Self::EW1 | Self::EW2 | Self::EW3 | Self::EW)
231    }
232
233    pub fn is_wv(&self) -> bool {
234        matches!(self, Self::WV1 | Self::WV2 | Self::WV)
235    }
236}
237
238#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
239#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
240pub enum DatasetPolarisation {
241    HH,
242    VV,
243    HV,
244    VH,
245}
246
247/// Sentinel 1 Dataset
248///
249/// Based on the [official S1 naming convention](https://sentinel.esa.int/web/sentinel/user-guides/sentinel-1-sar/naming-conventions).
250#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
251#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
252pub struct Dataset {
253    /// Mission id
254    ///
255    /// # Official description
256    ///
257    /// > The Mission Identifier (MMM) denotes the satellite and will be either
258    /// > S1A for the SENTINEL-1A instrument or S1B for the SENTINEL-1B instrument.
259    pub mission_id: MissionId,
260
261    /// swath identifier
262    ///
263    /// # Official description
264    ///
265    /// > The Swath Identifier (sss) identifies the s1-s6 beams for SM mode,
266    /// > iw1-iw3 for IW mode, ew1-ew5 for EW more and wv1-wv2 for WV mode.
267    pub swath_identifier: SwathIdentifier,
268
269    /// Product Type
270    ///
271    /// # Official description
272    ///
273    /// > Product Type (TTT) can be RAW, SLC, GRD or OCN.
274    pub product_type: ProductType,
275
276    /// Polarisation
277    ///
278    /// # Official description
279    ///
280    /// > Polarisation (PP) can be one of:
281    /// > * hh (single HH polarisation)
282    /// > * vv (single VV polarisation)
283    /// > * hv (single HV polarisation)
284    /// > * vh (single VH polarisation).
285    pub polarisation: DatasetPolarisation,
286
287    /// sensing start datetime
288    pub start_datetime: NaiveDateTime,
289
290    /// sensing top datetime
291    pub stop_datetime: NaiveDateTime,
292
293    /// Orbit number
294    ///
295    /// # Official description
296    ///
297    /// > The absolute orbit number at product start time (OOOOOO) will be
298    /// > in the range 000001-999999.
299    pub orbit_number: u32,
300
301    /// Data take identifier
302    ///
303    /// # Official description
304    ///
305    /// > The mission data-take identifier (DDDDDD) will be in the range 000001-FFFFFF.
306    pub data_take_identifier: String,
307
308    /// image number
309    ///
310    /// # Official description
311    ///
312    /// > The image number (nnn) identifies each individual image. WV
313    /// > vignettes each have their own image number as do each swath and polarization image for SM, IW and EW.
314    pub image_number: u32,
315}
316
317fn is_not_product_sep(c: core::primitive::char) -> bool {
318    c != '_'
319}
320
321fn consume_product_sep(s: &str) -> IResult<&str, core::primitive::char> {
322    char('_')(s)
323}
324
325fn consume_dataset_sep(s: &str) -> IResult<&str, core::primitive::char> {
326    char('-')(s)
327}
328
329fn parse_mission_id(s: &str) -> IResult<&str, MissionId> {
330    alt((
331        map(tag_no_case("s1a"), |_| MissionId::S1A),
332        map(tag_no_case("s1b"), |_| MissionId::S1B),
333    ))(s)
334}
335
336fn parse_mode(s: &str) -> IResult<&str, Mode> {
337    alt((
338        map(tag_no_case("iw"), |_| Mode::IW),
339        map(tag_no_case("ew"), |_| Mode::EW),
340        map(tag_no_case("wv"), |_| Mode::WV),
341        map(tag_no_case("s1"), |_| Mode::S1),
342        map(tag_no_case("s2"), |_| Mode::S2),
343        map(tag_no_case("s3"), |_| Mode::S3),
344        map(tag_no_case("s4"), |_| Mode::S4),
345        map(tag_no_case("s5"), |_| Mode::S5),
346        map(tag_no_case("s6"), |_| Mode::S6),
347    ))(s)
348}
349
350fn parse_product_type(s: &str) -> IResult<&str, ProductType> {
351    alt((
352        map(tag_no_case("grd"), |_| ProductType::GRD),
353        map(tag_no_case("ocn"), |_| ProductType::OCN),
354        map(tag_no_case("raw"), |_| ProductType::RAW),
355        map(tag_no_case("slc"), |_| ProductType::SLC),
356    ))(s)
357}
358
359fn parse_resolution(s: &str) -> IResult<&str, ResolutionClass> {
360    alt((
361        map(tag_no_case("f"), |_| ResolutionClass::Full),
362        map(tag_no_case("h"), |_| ResolutionClass::High),
363        map(tag_no_case("m"), |_| ResolutionClass::Medium),
364        map(tag_no_case("_"), |_| ResolutionClass::NotApplicable),
365    ))(s)
366}
367
368fn parse_processing_level(s: &str) -> IResult<&str, ProcessingLevel> {
369    alt((
370        map(tag("0"), |_| ProcessingLevel::Level0),
371        map(tag("1"), |_| ProcessingLevel::Level1),
372        map(tag("2"), |_| ProcessingLevel::Level2),
373    ))(s)
374}
375
376fn parse_product_class(s: &str) -> IResult<&str, ProductClass> {
377    alt((
378        map(tag_no_case("s"), |_| ProductClass::Standard),
379        map(tag_no_case("a"), |_| ProductClass::Annotation),
380    ))(s)
381}
382
383fn parse_product_polarisation(s: &str) -> IResult<&str, ProductPolarisation> {
384    alt((
385        map(tag_no_case("sh"), |_| ProductPolarisation::HH),
386        map(tag_no_case("sv"), |_| ProductPolarisation::VV),
387        map(tag_no_case("dh"), |_| ProductPolarisation::HHHV),
388        map(tag_no_case("dv"), |_| ProductPolarisation::VVVH),
389    ))(s)
390}
391
392/// nom parser function
393pub fn parse_product(s: &str) -> IResult<&str, Product> {
394    let (s, mission_id) = parse_mission_id(s)?;
395    let (s, _) = consume_product_sep(s)?;
396    let (s, mode) = parse_mode(s)?;
397    let (s, _) = consume_product_sep(s)?;
398    let (s, product_type) = parse_product_type(s)?;
399    let (s, resolution_class) = parse_resolution(s)?;
400    let (s, _) = consume_product_sep(s)?;
401    let (s, processing_level) = parse_processing_level(s)?;
402    let (s, product_class) = parse_product_class(s)?;
403    let (s, polarisation) = parse_product_polarisation(s)?;
404    let (s, _) = consume_product_sep(s)?;
405    let (s, start_datetime) = parse_esa_timestamp(s)?;
406    let (s, _) = consume_product_sep(s)?;
407    let (s, stop_datetime) = parse_esa_timestamp(s)?;
408    let (s, _) = consume_product_sep(s)?;
409    let (s, orbit_number) = take_n_digits_in_range(6, 1..=999999)(s)?;
410    let (s, _) = consume_product_sep(s)?;
411    let (s, data_take_identifier) = take_while_m_n(6, 6, is_not_product_sep)(s)?;
412    let (s, _) = consume_product_sep(s)?;
413    let (s, product_unique_identifier) = take_while_m_n(4, 4, is_not_product_sep)(s)?;
414
415    Ok((
416        s,
417        Product {
418            mission_id,
419            mode,
420            product_type,
421            resolution_class,
422            processing_level,
423            product_class,
424            polarisation,
425            start_datetime,
426            stop_datetime,
427            orbit_number,
428            data_take_identifier: data_take_identifier.to_uppercase(),
429            product_unique_identifier: product_unique_identifier.to_uppercase(),
430        },
431    ))
432}
433
434fn parse_dataset_polarisation(s: &str) -> IResult<&str, DatasetPolarisation> {
435    alt((
436        map(tag_no_case("hh"), |_| DatasetPolarisation::HH),
437        map(tag_no_case("vv"), |_| DatasetPolarisation::VV),
438        map(tag_no_case("hv"), |_| DatasetPolarisation::HV),
439        map(tag_no_case("vh"), |_| DatasetPolarisation::VH),
440    ))(s)
441}
442
443fn parse_swath_identifier(s: &str) -> IResult<&str, SwathIdentifier> {
444    alt((
445        map(tag_no_case("s1"), |_| SwathIdentifier::S1),
446        map(tag_no_case("s2"), |_| SwathIdentifier::S2),
447        map(tag_no_case("s3"), |_| SwathIdentifier::S3),
448        map(tag_no_case("s4"), |_| SwathIdentifier::S4),
449        map(tag_no_case("s5"), |_| SwathIdentifier::S5),
450        map(tag_no_case("s6"), |_| SwathIdentifier::S6),
451        map(tag_no_case("iw1"), |_| SwathIdentifier::IW1),
452        map(tag_no_case("iw2"), |_| SwathIdentifier::IW2),
453        map(tag_no_case("iw3"), |_| SwathIdentifier::IW3),
454        map(tag_no_case("iw"), |_| SwathIdentifier::IW),
455        map(tag_no_case("ew1"), |_| SwathIdentifier::EW1),
456        map(tag_no_case("ew2"), |_| SwathIdentifier::EW2),
457        map(tag_no_case("ew3"), |_| SwathIdentifier::EW3),
458        map(tag_no_case("ew4"), |_| SwathIdentifier::EW4),
459        map(tag_no_case("ew5"), |_| SwathIdentifier::EW5),
460        map(tag_no_case("ew"), |_| SwathIdentifier::EW),
461        map(tag_no_case("wv1"), |_| SwathIdentifier::WV1),
462        map(tag_no_case("wv2"), |_| SwathIdentifier::WV2),
463        map(tag_no_case("wv"), |_| SwathIdentifier::WV),
464    ))(s)
465}
466
467/// nom parser function
468pub fn parse_dataset(s: &str) -> IResult<&str, Dataset> {
469    let (s, mission_id) = parse_mission_id(s)?;
470    let (s, _) = consume_dataset_sep(s)?;
471    let (s, swath_identifier) = parse_swath_identifier(s)?;
472    let (s, _) = consume_dataset_sep(s)?;
473    let (s, product_type) = parse_product_type(s)?;
474    let (s, _) = consume_dataset_sep(s)?;
475    let (s, polarisation) = parse_dataset_polarisation(s)?;
476    let (s, _) = consume_dataset_sep(s)?;
477    let (s, start_datetime) = parse_esa_timestamp(s)?;
478    let (s, _) = consume_dataset_sep(s)?;
479    let (s, stop_datetime) = parse_esa_timestamp(s)?;
480    let (s, _) = consume_dataset_sep(s)?;
481    let (s, orbit_number) = take_n_digits_in_range(6, 1..=999999)(s)?;
482    let (s, _) = consume_dataset_sep(s)?;
483    let (s, data_take_identifier) = take_while_m_n(6, 6, is_not_product_sep)(s)?;
484    let (s, _) = consume_dataset_sep(s)?;
485    let (s, image_number) = take_n_digits_in_range(3, 0..=999)(s)?;
486
487    Ok((
488        s,
489        Dataset {
490            mission_id,
491            swath_identifier,
492            product_type,
493            polarisation,
494            start_datetime,
495            stop_datetime,
496            orbit_number,
497            data_take_identifier: data_take_identifier.to_uppercase(),
498            image_number,
499        },
500    ))
501}
502
503impl_from_str!(parse_dataset, Dataset);
504impl_from_str!(parse_product, Product);
505
506#[cfg(test)]
507mod tests {
508    use crate::identifiers::sentinel1::{
509        parse_dataset, parse_product, DatasetPolarisation, MissionId, Mode, ProcessingLevel,
510        ProductClass, ProductPolarisation, ProductType, ResolutionClass, SwathIdentifier,
511    };
512    use crate::identifiers::tests::apply_to_samples_from_txt;
513
514    #[test]
515    fn parse_s1_product() {
516        let (_, product) =
517            parse_product("S1A_IW_GRDH_1SDV_20200207T051836_20200207T051901_031142_039466_A237")
518                .unwrap();
519        assert_eq!(product.mission_id, MissionId::S1A);
520        assert_eq!(product.mode, Mode::IW);
521        assert_eq!(product.product_type, ProductType::GRD);
522        assert_eq!(product.resolution_class, ResolutionClass::High);
523        assert_eq!(product.processing_level, ProcessingLevel::Level1);
524        assert_eq!(product.product_class, ProductClass::Standard);
525        assert_eq!(product.polarisation, ProductPolarisation::VVVH);
526        // timestamps skipped
527        assert_eq!(product.orbit_number, 31142);
528        assert_eq!(product.data_take_identifier.as_str(), "039466");
529        assert_eq!(product.product_unique_identifier.as_str(), "A237");
530    }
531
532    #[test]
533    fn parse_s1_dataset() {
534        let (_, ds) =
535            parse_dataset("s1a-iw-grd-vh-20221029t171425-20221029t171450-045660-0575ce-002.tiff")
536                .unwrap();
537        assert_eq!(ds.mission_id, MissionId::S1A);
538        assert_eq!(ds.swath_identifier, SwathIdentifier::IW);
539        assert_eq!(ds.product_type, ProductType::GRD);
540        assert_eq!(ds.polarisation, DatasetPolarisation::VH);
541        // timestamps skipped
542        assert_eq!(ds.orbit_number, 45660);
543        assert_eq!(ds.data_take_identifier.as_str(), "0575CE");
544    }
545
546    #[test]
547    fn parse_s1_dataset_no_fileextension() {
548        let (_, _ds) =
549            parse_dataset("s1a-iw-grd-vh-20221029t171425-20221029t171450-045660-0575ce-002")
550                .unwrap();
551    }
552
553    #[test]
554    fn apply_to_product_testdata() {
555        apply_to_samples_from_txt("sentinel1_products.txt", |s| {
556            parse_product(s).unwrap();
557        })
558    }
559}