eo_identifiers/identifiers/
landsat.rs

1//! Landsat
2//!
3//! # Example
4//!
5//! ```rust
6//! use eo_identifiers::identifiers::landsat::{Product, SceneId};
7//! use std::str::FromStr;
8//!
9//! assert!(
10//!     Product::from_str("LC08_L2SP_008008_20180520_20200901_02_T2")
11//!     .is_ok()
12//! );
13//! assert!(
14//!     SceneId::from_str("LC80390222013076EDC00")
15//!     .is_ok()
16//! );
17//! ```
18use crate::common_parsers::{
19    date_year, parse_simple_date, take_alphanumeric, take_alphanumeric_n, take_n_digits,
20    take_n_digits_in_range,
21};
22use crate::{impl_from_str, Mission, Name, NameLong};
23use chrono::{Duration, NaiveDate};
24use nom::branch::alt;
25use nom::bytes::complete::{tag, tag_no_case, take};
26use nom::combinator::{map, opt};
27use nom::error::ErrorKind;
28use nom::sequence::tuple;
29use nom::IResult;
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub enum MissionId {
36    Landsat1,
37    Landsat2,
38    Landsat3,
39    Landsat4,
40    Landsat5,
41    Landsat6,
42    Landsat7,
43    Landsat8,
44    Landsat9,
45}
46
47impl From<u8> for MissionId {
48    fn from(v: u8) -> Self {
49        match v {
50            1 => Self::Landsat1,
51            2 => Self::Landsat2,
52            3 => Self::Landsat3,
53            4 => Self::Landsat4,
54            5 => Self::Landsat5,
55            6 => Self::Landsat6,
56            7 => Self::Landsat7,
57            8 => Self::Landsat8,
58            9 => Self::Landsat9,
59            _ => panic!("invalid landsat satellite number"),
60        }
61    }
62}
63
64impl From<MissionId> for Mission {
65    fn from(mission: MissionId) -> Self {
66        match mission {
67            MissionId::Landsat1 => Self::Landsat1,
68            MissionId::Landsat2 => Self::Landsat2,
69            MissionId::Landsat3 => Self::Landsat3,
70            MissionId::Landsat4 => Self::Landsat4,
71            MissionId::Landsat5 => Self::Landsat5,
72            MissionId::Landsat6 => Self::Landsat6,
73            MissionId::Landsat7 => Self::Landsat7,
74            MissionId::Landsat8 => Self::Landsat8,
75            MissionId::Landsat9 => Self::Landsat9,
76        }
77    }
78}
79
80#[allow(non_camel_case_types)]
81#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Copy, Hash)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub enum Sensor {
84    /// C = OLI & TIRS
85    OLI_TRIS,
86
87    /// O = OLI only
88    OLI,
89
90    /// T = IRS only
91    IRS,
92
93    /// E = ETM+
94    ETM_PLUS,
95
96    /// T = TM
97    TM,
98    /// M = MSS
99    MSS,
100}
101
102impl Name for Sensor {
103    fn name(&self) -> &str {
104        // https://en.wikipedia.org/wiki/Landsat_program
105        match self {
106            Sensor::OLI_TRIS => "OLI+TRIS",
107            Sensor::OLI => "OLI",
108            Sensor::IRS => "IRS",
109            Sensor::ETM_PLUS => "ETM+",
110            Sensor::TM => "TM",
111            Sensor::MSS => "MSS",
112        }
113    }
114}
115
116impl NameLong for Sensor {
117    fn name_long(&self) -> &str {
118        // https://en.wikipedia.org/wiki/Landsat_program
119        match self {
120            Sensor::OLI_TRIS => "Operational Land Imager+TRIS",
121            Sensor::OLI => "Operational Land Imager",
122            Sensor::IRS => "InfraRed Sensor",
123            Sensor::ETM_PLUS => "Enhanced Thematic Mapper Plus",
124            Sensor::TM => "Thematic Mapper",
125            Sensor::MSS => "Multi Spectral Scanner",
126        }
127    }
128}
129
130fn parse_julian_date(s: &str) -> IResult<&str, NaiveDate> {
131    let (s, year) = date_year(s)?;
132    let (s_out, day_of_year) = take_n_digits::<i64>(3)(s)?;
133    let date = NaiveDate::from_ymd_opt(year, 1, 1)
134        .ok_or_else(|| nom::Err::Error(nom::error::Error::new(s, ErrorKind::Fail)))?
135        + Duration::days(day_of_year - 1);
136    Ok((s_out, date))
137}
138
139/// Landsat scene id
140///
141/// <https://gisgeography.com/landsat-file-naming-convention/>
142/// <https://www.usgs.gov/faqs/what-naming-convention-landsat-collections-level-1-scenes>
143/// <https://www.usgs.gov/faqs/what-naming-convention-landsat-collection-2-level-1-and-level-2-scenes>
144#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
145#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
146pub struct SceneId {
147    /// sensor
148    pub sensor: Sensor,
149
150    /// satellite
151    pub mission: MissionId,
152
153    pub wrs_path: u32,
154    pub wrs_row: u32,
155
156    pub acquire_date: NaiveDate,
157
158    pub ground_station_identifier: String,
159    pub archive_version_number: u8,
160}
161
162fn parse_sensor(s: &str, mission: u8) -> IResult<&str, Sensor> {
163    alt((
164        map(tag_no_case("c"), |_| Sensor::OLI_TRIS),
165        map(tag_no_case("o"), |_| Sensor::OLI),
166        map(tag_no_case("t"), |_| {
167            // T = TM for Landsat 4 & 5)
168            if mission == 4 || mission == 5 {
169                Sensor::TM
170            } else {
171                Sensor::IRS
172            }
173        }),
174        map(tag_no_case("e"), |_| Sensor::ETM_PLUS),
175        map(tag_no_case("m"), |_| Sensor::MSS),
176    ))(s)
177}
178
179/// nom parser function
180pub fn parse_scene_id(s: &str) -> IResult<&str, SceneId> {
181    let (s_sensor, _) = tag_no_case("L")(s)?;
182    let (s, _) = take(1usize)(s_sensor)?;
183    let (s, mission): (&str, u8) = take_n_digits_in_range(1, 1..=9)(s)?;
184    let (_, sensor) = parse_sensor(s_sensor, mission)?;
185    let (s, wrs_path) = take_n_digits(3)(s)?;
186    let (s, wrs_row) = take_n_digits(3)(s)?;
187    let (s, acquire_date) = parse_julian_date(s)?;
188    let (s, ground_station_identifier) = take_alphanumeric_n(3)(s)?;
189    let (s, archive_version_number) = take_n_digits(2)(s)?;
190    Ok((
191        s,
192        SceneId {
193            sensor,
194            mission: mission.into(),
195            wrs_path,
196            wrs_row,
197            acquire_date,
198            ground_station_identifier: ground_station_identifier.to_uppercase(),
199            archive_version_number,
200        },
201    ))
202}
203
204///
205/// CU, AK, HI see <https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/atoms/files/LSDS-1609_Landsat-Tile-Full-Resolution-Browse_Data-Control-Book-v1.pdf>
206#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
207#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
208pub enum ProcessingLevel {
209    L1TP,
210    L1GT,
211    L1GS,
212    L2SP,
213    L2SR,
214    /// CONUS
215    CU,
216    /// Alaska
217    AK,
218    /// Hawaii
219    HI,
220    Other(String),
221}
222
223#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash, Copy)]
224#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
225pub enum CollectionCategory {
226    RealTime,
227    Tier1,
228    Tier2,
229    AlbersTier1,
230    AlbersTier2,
231}
232
233impl Name for CollectionCategory {
234    fn name(&self) -> &str {
235        match self {
236            CollectionCategory::RealTime => "RT",
237            CollectionCategory::Tier1 => "T1",
238            CollectionCategory::Tier2 => "T2",
239            CollectionCategory::AlbersTier1 => "A1",
240            CollectionCategory::AlbersTier2 => "A2",
241        }
242    }
243}
244
245impl NameLong for CollectionCategory {
246    fn name_long(&self) -> &str {
247        match self {
248            CollectionCategory::RealTime => "Real-Time",
249            CollectionCategory::Tier1 => "Tier 1",
250            CollectionCategory::Tier2 => "Tier 2",
251            CollectionCategory::AlbersTier1 => "Albers Tier 1",
252            CollectionCategory::AlbersTier2 => "Albers Tier 2",
253        }
254    }
255}
256
257/// Landsat product
258///
259/// <https://gisgeography.com/landsat-file-naming-convention/>
260/// <https://www.usgs.gov/faqs/what-naming-convention-landsat-collections-level-1-scenes>
261/// <https://www.usgs.gov/faqs/what-naming-convention-landsat-collection-2-level-1-and-level-2-scenes>
262#[derive(PartialOrd, PartialEq, Eq, Debug, Clone, Hash)]
263#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
264pub struct Product {
265    /// sensor
266    pub sensor: Sensor,
267
268    /// satellite
269    pub mission: MissionId,
270
271    /// processing correction level
272    pub processing_level: ProcessingLevel,
273
274    pub wrs_path: u32,
275    pub wrs_row: u32,
276    pub acquire_date: NaiveDate,
277    pub processing_date: NaiveDate,
278    pub collection_number: u8,
279    pub collection_category: Option<CollectionCategory>,
280}
281
282fn consume_product_sep(s: &str) -> IResult<&str, &str> {
283    tag("_")(s)
284}
285
286fn parse_processing_level(s: &str) -> IResult<&str, ProcessingLevel> {
287    alt((
288        map(tag_no_case("l1tp"), |_| ProcessingLevel::L1TP),
289        map(tag_no_case("l1gs"), |_| ProcessingLevel::L1GS),
290        map(tag_no_case("l1gt"), |_| ProcessingLevel::L1GT),
291        map(tag_no_case("l2sp"), |_| ProcessingLevel::L2SP),
292        map(tag_no_case("l2sr"), |_| ProcessingLevel::L2SR),
293        map(tag_no_case("cu"), |_| ProcessingLevel::CU),
294        map(tag_no_case("ak"), |_| ProcessingLevel::AK),
295        map(tag_no_case("hi"), |_| ProcessingLevel::HI),
296        map(take_alphanumeric, |pl| {
297            ProcessingLevel::Other(pl.to_uppercase())
298        }),
299    ))(s)
300}
301
302fn parse_collection_category(s: &str) -> IResult<&str, CollectionCategory> {
303    alt((
304        map(tag_no_case("rt"), |_| CollectionCategory::RealTime),
305        map(tag_no_case("t1"), |_| CollectionCategory::Tier1),
306        map(tag_no_case("t2"), |_| CollectionCategory::Tier2),
307        map(tag_no_case("a1"), |_| CollectionCategory::AlbersTier1),
308        map(tag_no_case("a2"), |_| CollectionCategory::AlbersTier2),
309    ))(s)
310}
311
312/// nom parser function
313pub fn parse_product(s: &str) -> IResult<&str, Product> {
314    let (s_sensor, _) = tag_no_case("L")(s)?;
315    let (s, _) = take(1usize)(s_sensor)?;
316    let (s, _) = tag("0")(s)?;
317    let (s, mission): (&str, u8) = take_n_digits_in_range(1, 1..=9)(s)?;
318    let (_, sensor) = parse_sensor(s_sensor, mission)?;
319    let (s, _) = consume_product_sep(s)?;
320    let (s, processing_level) = parse_processing_level(s)?;
321    let (s, _) = consume_product_sep(s)?;
322    let (s, wrs_path) = take_n_digits(3)(s)?;
323    let (s, wrs_row) = take_n_digits(3)(s)?;
324    let (s, _) = consume_product_sep(s)?;
325    let (s, acquire_date) = parse_simple_date(s)?;
326    let (s, _) = consume_product_sep(s)?;
327    let (s, processing_date) = parse_simple_date(s)?;
328    let (s, _) = consume_product_sep(s)?;
329    let (s, collection_number) = take_n_digits(2)(s)?;
330    let (s, collection_category) = map(
331        opt(tuple((consume_product_sep, parse_collection_category))),
332        |cc| cc.map(|cc| cc.1),
333    )(s)?;
334    Ok((
335        s,
336        Product {
337            sensor,
338            mission: mission.into(),
339            processing_level,
340            wrs_path,
341            wrs_row,
342            acquire_date,
343            processing_date,
344            collection_number,
345            collection_category,
346        },
347    ))
348}
349
350impl_from_str!(parse_product, Product);
351impl_from_str!(parse_scene_id, SceneId);
352
353#[cfg(test)]
354mod tests {
355    use crate::identifiers::landsat::{
356        parse_julian_date, parse_product, parse_scene_id, CollectionCategory, MissionId,
357        ProcessingLevel, Sensor,
358    };
359    use crate::identifiers::tests::apply_to_samples_from_txt;
360    use chrono::NaiveDate;
361
362    #[test]
363    fn test_parse_julian_date() {
364        let (_, d) = parse_julian_date("2020046").unwrap();
365        assert_eq!(d, NaiveDate::from_ymd_opt(2020, 2, 15).unwrap());
366    }
367
368    #[test]
369    fn test_parse_scene() {
370        let (_, scene) = parse_scene_id("LC80390222013076EDC00").unwrap();
371        assert_eq!(scene.sensor, Sensor::OLI_TRIS);
372        assert_eq!(scene.mission, MissionId::Landsat8);
373        assert_eq!(scene.wrs_path, 39);
374        assert_eq!(scene.wrs_row, 22);
375        assert_eq!(
376            scene.acquire_date,
377            NaiveDate::from_ymd_opt(2013, 3, 17).unwrap()
378        );
379        assert_eq!(scene.ground_station_identifier.as_str(), "EDC");
380        assert_eq!(scene.archive_version_number, 0);
381    }
382
383    #[test]
384    fn test_parse_product_l1() {
385        let (_, product) = parse_product("LC08_L1GT_029030_20151209_20160131_01_RT").unwrap();
386        assert_eq!(product.sensor, Sensor::OLI_TRIS);
387        assert_eq!(product.mission, MissionId::Landsat8);
388        assert_eq!(product.processing_level, ProcessingLevel::L1GT);
389        assert_eq!(
390            product.collection_category,
391            Some(CollectionCategory::RealTime)
392        );
393    }
394
395    #[test]
396    fn test_parse_product_l2() {
397        let (_, product) = parse_product("LC08_L2SP_140041_20130503_20190828_02_T1").unwrap();
398        assert_eq!(product.sensor, Sensor::OLI_TRIS);
399        assert_eq!(product.mission, MissionId::Landsat8);
400        assert_eq!(product.processing_level, ProcessingLevel::L2SP);
401        assert_eq!(product.collection_category, Some(CollectionCategory::Tier1));
402    }
403
404    #[test]
405    fn apply_to_product_testdata() {
406        apply_to_samples_from_txt("landsat_products.txt", |s| {
407            parse_product(s).unwrap();
408        })
409    }
410}