eo_identifiers/identifiers/
sentinel2.rs1use 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#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
52#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53pub struct Product {
54 pub mission_id: MissionId,
56
57 pub product_level: ProductLevel,
59
60 pub start_datetime: NaiveDateTime,
62
63 pub pdgs_baseline_number: (u8, u8),
65
66 pub relative_orbit_number: u8,
68
69 pub tile_number: String,
71
72 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
117pub 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 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}