eo_identifiers/identifiers/
sentinel2.rs

1//! Sentinel 2
2//!
3//! # Example
4//!
5//! ```rust
6//! use eo_identifiers::identifiers::sentinel2::Product;
7//! use std::str::FromStr;
8//!
9//! assert!(
10//!     Product::from_str("S2A_MSIL1C_20170105T013442_N0204_R031_T53NMJ_20170105T013443")
11//!     .is_ok()
12//! );
13//! ```
14use chrono::NaiveDateTime;
15use nom::branch::alt;
16use nom::bytes::complete::tag_no_case;
17use nom::character::complete::char;
18use nom::combinator::map;
19use nom::IResult;
20
21use crate::common_parsers::{parse_esa_timestamp, take_alphanumeric_n, take_n_digits_in_range};
22use crate::{impl_from_str, Mission};
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub enum MissionId {
29    S2A,
30    S2B,
31}
32
33impl From<MissionId> for Mission {
34    fn from(_: MissionId) -> Self {
35        Mission::Sentinel2
36    }
37}
38
39#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41pub enum ProductLevel {
42    L1C,
43    L2A,
44}
45
46/// Sentinel 2 product
47///
48/// New format Naming Convention for Sentinel-2 Level-1C products generated after 6 December 2016:
49///
50/// [naming convention](https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi/naming-convention)
51#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
52#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53pub struct Product {
54    /// mission id
55    pub mission_id: MissionId,
56
57    /// product level
58    pub product_level: ProductLevel,
59
60    /// sensing start datetime
61    pub start_datetime: NaiveDateTime,
62
63    /// PDGS Processing Baseline number
64    pub pdgs_baseline_number: (u8, u8),
65
66    /// Relative Orbit number (R001 - R143)
67    pub relative_orbit_number: u8,
68
69    /// tile number
70    pub tile_number: String,
71
72    /// Product Discriminator
73    ///
74    /// Used to distinguish between different end user products from the same datatake.
75    /// Depending on the instance, the time in this field can be earlier or slightly later than
76    /// the datatake sensing time.
77    pub product_discriminator: String,
78}
79
80fn consume_product_sep(s: &str) -> IResult<&str, core::primitive::char> {
81    char('_')(s)
82}
83
84fn parse_mission_id(s: &str) -> IResult<&str, MissionId> {
85    alt((
86        map(tag_no_case("s2a"), |_| MissionId::S2A),
87        map(tag_no_case("s2b"), |_| MissionId::S2B),
88    ))(s)
89}
90
91fn parse_product_level(s: &str) -> IResult<&str, ProductLevel> {
92    alt((
93        map(tag_no_case("l1c"), |_| ProductLevel::L1C),
94        map(tag_no_case("l2a"), |_| ProductLevel::L2A),
95    ))(s)
96}
97
98fn parse_processing_baseline_number(s: &str) -> IResult<&str, (u8, u8)> {
99    let (s, _) = tag_no_case("n")(s)?;
100    let (s, x) = take_n_digits_in_range(2, 0..=99)(s)?;
101    let (s, y) = take_n_digits_in_range(2, 0..=99)(s)?;
102    Ok((s, (x, y)))
103}
104
105fn parse_relative_orbit_number(s: &str) -> IResult<&str, u8> {
106    let (s, _) = tag_no_case("r")(s)?;
107    let (s, ron) = take_n_digits_in_range(3, 1..=143)(s)?;
108    Ok((s, ron))
109}
110
111fn parse_tile_number(s: &str) -> IResult<&str, String> {
112    let (s, _) = tag_no_case("t")(s)?;
113    let (s, tn) = take_alphanumeric_n(5)(s)?;
114    Ok((s, tn.to_uppercase()))
115}
116
117/// nom parser function
118/// parse new format Naming Convention for Sentinel-2 Level-1C products generated after 6 December 2016:
119pub fn parse_product(s: &str) -> IResult<&str, Product> {
120    let (s, mission_id) = parse_mission_id(s)?;
121    let (s, _) = consume_product_sep(s)?;
122    let (s, _) = tag_no_case("msi")(s)?;
123    let (s, product_level) = parse_product_level(s)?;
124    let (s, _) = consume_product_sep(s)?;
125    let (s, start_datetime) = parse_esa_timestamp(s)?;
126    let (s, _) = consume_product_sep(s)?;
127    let (s, pdgs_baseline_number) = parse_processing_baseline_number(s)?;
128    let (s, _) = consume_product_sep(s)?;
129    let (s, relative_orbit_number) = parse_relative_orbit_number(s)?;
130    let (s, _) = consume_product_sep(s)?;
131    let (s, tile_number) = parse_tile_number(s)?;
132    let (s, _) = consume_product_sep(s)?;
133    let (s, product_discriminator) = take_alphanumeric_n(15)(s)?;
134
135    Ok((
136        s,
137        Product {
138            mission_id,
139            product_level,
140            start_datetime,
141            pdgs_baseline_number,
142            relative_orbit_number,
143            tile_number,
144            product_discriminator: product_discriminator.to_uppercase(),
145        },
146    ))
147}
148
149impl_from_str!(parse_product, Product);
150
151#[cfg(test)]
152mod tests {
153    use crate::identifiers::sentinel2::{parse_product, MissionId, Product, ProductLevel};
154    use crate::identifiers::tests::apply_to_samples_from_txt;
155    use std::str::FromStr;
156
157    #[test]
158    fn parse_s2_product() {
159        let (_, product) =
160            parse_product("S2A_MSIL1C_20170105T013442_N0204_R031_T53NMJ_20170105T013443.SAFE")
161                .unwrap();
162        assert_eq!(product.mission_id, MissionId::S2A);
163        assert_eq!(product.product_level, ProductLevel::L1C);
164        // timestamp omitted
165        assert_eq!(product.pdgs_baseline_number, (2, 4));
166        assert_eq!(product.relative_orbit_number, 31);
167        assert_eq!(product.tile_number.as_str(), "53NMJ");
168        assert_eq!(product.product_discriminator.as_str(), "20170105T013443");
169    }
170
171    #[test]
172    fn apply_to_product_testdata() {
173        apply_to_samples_from_txt("sentinel2_products.txt", |s| {
174            parse_product(s).unwrap();
175        })
176    }
177
178    #[test]
179    fn test_from_str() {
180        assert!(
181            Product::from_str("S2A_MSIL1C_20170105T013442_N0204_R031_T53NMJ_20170105T013443")
182                .is_ok()
183        );
184    }
185}