mapchete-eo 2025.7.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/archives/__init__.py +0 -0
  3. mapchete_eo/archives/base.py +65 -0
  4. mapchete_eo/array/__init__.py +0 -0
  5. mapchete_eo/array/buffer.py +16 -0
  6. mapchete_eo/array/color.py +29 -0
  7. mapchete_eo/array/convert.py +157 -0
  8. mapchete_eo/base.py +528 -0
  9. mapchete_eo/blacklist.txt +175 -0
  10. mapchete_eo/cli/__init__.py +30 -0
  11. mapchete_eo/cli/bounds.py +22 -0
  12. mapchete_eo/cli/options_arguments.py +243 -0
  13. mapchete_eo/cli/s2_brdf.py +77 -0
  14. mapchete_eo/cli/s2_cat_results.py +146 -0
  15. mapchete_eo/cli/s2_find_broken_products.py +93 -0
  16. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  17. mapchete_eo/cli/s2_mask.py +71 -0
  18. mapchete_eo/cli/s2_mgrs.py +45 -0
  19. mapchete_eo/cli/s2_rgb.py +114 -0
  20. mapchete_eo/cli/s2_verify.py +129 -0
  21. mapchete_eo/cli/static_catalog.py +123 -0
  22. mapchete_eo/eostac.py +30 -0
  23. mapchete_eo/exceptions.py +87 -0
  24. mapchete_eo/geometry.py +271 -0
  25. mapchete_eo/image_operations/__init__.py +12 -0
  26. mapchete_eo/image_operations/color_correction.py +136 -0
  27. mapchete_eo/image_operations/compositing.py +247 -0
  28. mapchete_eo/image_operations/dtype_scale.py +43 -0
  29. mapchete_eo/image_operations/fillnodata.py +130 -0
  30. mapchete_eo/image_operations/filters.py +319 -0
  31. mapchete_eo/image_operations/linear_normalization.py +81 -0
  32. mapchete_eo/image_operations/sigmoidal.py +114 -0
  33. mapchete_eo/io/__init__.py +37 -0
  34. mapchete_eo/io/assets.py +492 -0
  35. mapchete_eo/io/items.py +147 -0
  36. mapchete_eo/io/levelled_cubes.py +228 -0
  37. mapchete_eo/io/path.py +144 -0
  38. mapchete_eo/io/products.py +413 -0
  39. mapchete_eo/io/profiles.py +45 -0
  40. mapchete_eo/known_catalogs.py +42 -0
  41. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  42. mapchete_eo/platforms/sentinel2/archives.py +190 -0
  43. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  44. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  45. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  46. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  47. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  48. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  49. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  50. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  51. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  52. mapchete_eo/platforms/sentinel2/config.py +181 -0
  53. mapchete_eo/platforms/sentinel2/driver.py +78 -0
  54. mapchete_eo/platforms/sentinel2/masks.py +325 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
  56. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
  57. mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
  58. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
  59. mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
  60. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
  63. mapchete_eo/platforms/sentinel2/product.py +669 -0
  64. mapchete_eo/platforms/sentinel2/types.py +109 -0
  65. mapchete_eo/processes/__init__.py +0 -0
  66. mapchete_eo/processes/config.py +51 -0
  67. mapchete_eo/processes/dtype_scale.py +112 -0
  68. mapchete_eo/processes/eo_to_xarray.py +19 -0
  69. mapchete_eo/processes/merge_rasters.py +235 -0
  70. mapchete_eo/product.py +278 -0
  71. mapchete_eo/protocols.py +56 -0
  72. mapchete_eo/search/__init__.py +14 -0
  73. mapchete_eo/search/base.py +222 -0
  74. mapchete_eo/search/config.py +42 -0
  75. mapchete_eo/search/s2_mgrs.py +314 -0
  76. mapchete_eo/search/stac_search.py +251 -0
  77. mapchete_eo/search/stac_static.py +236 -0
  78. mapchete_eo/search/utm_search.py +251 -0
  79. mapchete_eo/settings.py +24 -0
  80. mapchete_eo/sort.py +48 -0
  81. mapchete_eo/time.py +53 -0
  82. mapchete_eo/types.py +73 -0
  83. mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
  84. mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
  85. mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
  86. mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
  87. mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,29 @@
1
+ from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
2
+ from mapchete_eo.platforms.sentinel2.path_mappers.earthsearch import (
3
+ EarthSearchPathMapper,
4
+ )
5
+ from mapchete_eo.platforms.sentinel2.path_mappers.metadata_xml import XMLMapper
6
+ from mapchete_eo.platforms.sentinel2.path_mappers.sinergise import SinergisePathMapper
7
+
8
+
9
+ def default_path_mapper_guesser(
10
+ url: str,
11
+ **kwargs,
12
+ ) -> S2PathMapper:
13
+ """Guess S2PathMapper based on URL.
14
+
15
+ If a new path mapper is added in this module, it should also be added to this function
16
+ in order to be detected.
17
+ """
18
+ if url.startswith(
19
+ ("https://roda.sentinel-hub.com/sentinel-s2-l2a/", "s3://sentinel-s2-l2a/")
20
+ ) or url.startswith(
21
+ ("https://roda.sentinel-hub.com/sentinel-s2-l1c/", "s3://sentinel-s2-l1c/")
22
+ ):
23
+ return SinergisePathMapper(url, **kwargs)
24
+ elif url.startswith(
25
+ "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/"
26
+ ):
27
+ return EarthSearchPathMapper(url, **kwargs)
28
+ else:
29
+ return XMLMapper(url, **kwargs)
@@ -0,0 +1,56 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from mapchete.path import MPath
4
+
5
+ from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
6
+ from mapchete_eo.platforms.sentinel2.types import (
7
+ BandQI,
8
+ L2ABand,
9
+ ProductQI,
10
+ ProductQIMaskResolution,
11
+ )
12
+
13
+
14
+ class S2PathMapper(ABC):
15
+ """
16
+ Abstract class to help mapping asset paths from metadata.xml to their
17
+ locations of various data archives.
18
+
19
+ This is mainly used for additional data like QI masks.
20
+ """
21
+
22
+ # All available bands for Sentinel-2 Level 2A.
23
+ _bands = [band.name for band in L2ABand]
24
+
25
+ processing_baseline: ProcessingBaseline
26
+
27
+ @abstractmethod
28
+ def product_qi_mask(
29
+ self,
30
+ qi_mask: ProductQI,
31
+ resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
32
+ ) -> MPath: ...
33
+
34
+ @abstractmethod
35
+ def classification_mask(self) -> MPath: ...
36
+
37
+ @abstractmethod
38
+ def cloud_probability_mask(
39
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
40
+ ) -> MPath: ...
41
+
42
+ @abstractmethod
43
+ def snow_probability_mask(
44
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
45
+ ) -> MPath: ...
46
+
47
+ @abstractmethod
48
+ def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath: ...
49
+
50
+ @abstractmethod
51
+ def technical_quality_mask(self, band: L2ABand) -> MPath: ...
52
+
53
+ @abstractmethod
54
+ def detector_footprints(self, band: L2ABand) -> MPath: ...
55
+
56
+ def clear_cached_data(self) -> None: ...
@@ -0,0 +1,34 @@
1
+ from mapchete.path import MPath
2
+
3
+ from mapchete_eo.platforms.sentinel2.path_mappers.sinergise import SinergisePathMapper
4
+ from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
5
+
6
+
7
+ class EarthSearchPathMapper(SinergisePathMapper):
8
+ """
9
+ The COG archive maintained by E84 and covered by EarthSearch does not hold additional data
10
+ such as the GML files. This class maps the metadata masks to the current EarthSearch product.
11
+
12
+ e.g.:
13
+ B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_DETFOO_B01.gml
14
+ Cloud masks: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_CLOUDS_B00.gml
15
+
16
+ newer products however:
17
+ B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2022/6/6/0/qi/DETFOO_B01.jp2
18
+ no vector cloudmasks available anymore
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ metadata_xml: MPath,
24
+ alternative_metadata_baseurl: str = "sentinel-s2-l2a",
25
+ protocol: str = "s3",
26
+ baseline_version: str = "04.00",
27
+ **kwargs,
28
+ ):
29
+ basedir = metadata_xml.parent
30
+ self._path = (basedir / "tileinfo_metadata.json").read_json()["path"]
31
+ self._utm_zone, self._latitude_band, self._grid_square = basedir.elements[-6:-3]
32
+ self._baseurl = alternative_metadata_baseurl
33
+ self._protocol = protocol
34
+ self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
@@ -0,0 +1,135 @@
1
+ """
2
+ A path mapper maps from an metadata XML file to additional metadata
3
+ on a given archive or a local SAFE file.
4
+ """
5
+
6
+ import logging
7
+ from xml.etree.ElementTree import Element
8
+ from functools import cached_property
9
+ from typing import Optional
10
+
11
+ from mapchete.path import MPath
12
+
13
+ from mapchete_eo.io import open_xml
14
+ from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
15
+ from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
16
+ from mapchete_eo.platforms.sentinel2.types import (
17
+ BandQI,
18
+ L2ABand,
19
+ ProductQI,
20
+ ProductQIMaskResolution,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class XMLMapper(S2PathMapper):
27
+ def __init__(
28
+ self, metadata_xml: MPath, xml_root: Optional[Element] = None, **kwargs
29
+ ):
30
+ self.metadata_xml = metadata_xml
31
+ self._cached_xml_root = xml_root
32
+ self._metadata_dir = metadata_xml.parent
33
+
34
+ def clear_cached_data(self):
35
+ if self._cached_xml_root is not None:
36
+ logger.debug("clear XMLMapper xml cache")
37
+ self._cached_xml_root.clear()
38
+ self._cached_xml_root = None
39
+
40
+ @property
41
+ def xml_root(self) -> Element:
42
+ if self._cached_xml_root is None:
43
+ self._cached_xml_root = open_xml(self.metadata_xml)
44
+ return self._cached_xml_root
45
+
46
+ @cached_property
47
+ def processing_baseline(self):
48
+ # try to guess processing baseline from product id
49
+ def _get_version(tag="TILE_ID"):
50
+ product_id = next(self.xml_root.iter(tag)).text
51
+ appendix = product_id.split("_")[-1]
52
+ if appendix.startswith("N"):
53
+ return appendix.lstrip("N")
54
+
55
+ version = _get_version()
56
+ try:
57
+ return ProcessingBaseline.from_version(version)
58
+ except Exception: # pragma: no cover
59
+ # try use L1C product version as fallback
60
+ # we don't need to test this because HOPEFULLY we won't be confronted
61
+ # with such data
62
+ try:
63
+ l1c_version = _get_version("L1C_TILE_ID")
64
+ except StopIteration:
65
+ l1c_version = "02.06"
66
+ if l1c_version is not None:
67
+ return ProcessingBaseline.from_version(f"{l1c_version}")
68
+
69
+ def product_qi_mask(
70
+ self,
71
+ qi_mask: ProductQI,
72
+ resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
73
+ ) -> MPath:
74
+ """Determine product QI mask from metadata.xml."""
75
+ qi_mask_type = dict(self.processing_baseline.product_mask_types)[qi_mask]
76
+ for i in self.xml_root.iter():
77
+ if i.tag == "MASK_FILENAME" and i.get("type") == qi_mask_type:
78
+ path = self._metadata_dir / i.text
79
+ if qi_mask == ProductQI.classification:
80
+ return path
81
+ else:
82
+ if resolution.name in path.name:
83
+ return path
84
+ else:
85
+ raise KeyError(f"no {qi_mask_type} with item found in metadata")
86
+
87
+ def classification_mask(self) -> MPath:
88
+ return self.product_qi_mask(ProductQI.classification)
89
+
90
+ def cloud_probability_mask(
91
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
92
+ ) -> MPath:
93
+ return self.product_qi_mask(ProductQI.cloud_probability, resolution=resolution)
94
+
95
+ def snow_probability_mask(
96
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
97
+ ) -> MPath:
98
+ return self.product_qi_mask(ProductQI.snow_probability, resolution=resolution)
99
+
100
+ def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath:
101
+ """Determine band QI mask from metadata.xml."""
102
+ if qi_mask.name not in dict(self.processing_baseline.band_mask_types).keys():
103
+ raise DeprecationWarning(
104
+ f"QI mask '{qi_mask}' not available for this product"
105
+ )
106
+ mask_types = set()
107
+ for masks in self.xml_root.iter("Pixel_Level_QI"):
108
+ if masks.get("geometry") == "FULL_RESOLUTION":
109
+ for mask_path in masks:
110
+ qi_mask_type = dict(self.processing_baseline.band_mask_types)[
111
+ qi_mask
112
+ ]
113
+ mask_type = mask_path.get("type")
114
+ if mask_type:
115
+ mask_types.add(mask_type)
116
+ if mask_type == qi_mask_type:
117
+ band_id = mask_path.get("bandId")
118
+ if band_id is not None:
119
+ band_idx = int(band_id)
120
+ if band_idx == band.value:
121
+ return self._metadata_dir / mask_path.text
122
+ else: # pragma: no cover
123
+ raise KeyError(
124
+ f"no {qi_mask_type} for band {band.name} not found in metadata: {', '.join(mask_types)}"
125
+ )
126
+ else: # pragma: no cover
127
+ raise KeyError(
128
+ f"no {qi_mask_type} not found in metadata: {', '.join(mask_types)}"
129
+ )
130
+
131
+ def technical_quality_mask(self, band: L2ABand) -> MPath:
132
+ return self.band_qi_mask(BandQI.technical_quality, band)
133
+
134
+ def detector_footprints(self, band: L2ABand) -> MPath:
135
+ return self.band_qi_mask(BandQI.detector_footprints, band)
@@ -0,0 +1,105 @@
1
+ from mapchete.path import MPath, MPathLike
2
+
3
+ from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
4
+ from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
5
+ from mapchete_eo.platforms.sentinel2.types import (
6
+ BandQI,
7
+ L2ABand,
8
+ ProductQI,
9
+ ProductQIMaskResolution,
10
+ )
11
+
12
+
13
+ class SinergisePathMapper(S2PathMapper):
14
+ """
15
+ Return true paths of product quality assets from the Sinergise S2 bucket.
16
+
17
+ e.g.:
18
+ B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_DETFOO_B01.gml
19
+ Cloud masks: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_CLOUDS_B00.gml
20
+
21
+ newer products however:
22
+ B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2022/6/6/0/qi/DETFOO_B01.jp2
23
+ no vector cloudmasks available anymore
24
+ """
25
+
26
+ _PRE_0400_MASK_PATHS = {
27
+ ProductQI.classification: "MSK_CLOUDS_B00.gml",
28
+ ProductQI.cloud_probability: "CLD_{resolution}.jp2", # are they really there?
29
+ ProductQI.snow_probability: "SNW_{resolution}.jp2", # are they really there?
30
+ BandQI.detector_footprints: "MSK_DETFOO_{band_identifier}.gml",
31
+ BandQI.technical_quality: "MSK_TECQUA_{band_identifier}.gml",
32
+ }
33
+ _POST_0400_MASK_PATHS = {
34
+ ProductQI.classification: "CLASSI_B00.jp2",
35
+ ProductQI.cloud_probability: "CLD_{resolution}.jp2",
36
+ ProductQI.snow_probability: "SNW_{resolution}.jp2",
37
+ BandQI.detector_footprints: "DETFOO_{band_identifier}.jp2",
38
+ BandQI.technical_quality: "QUALIT_{band_identifier}.jp2",
39
+ }
40
+
41
+ def __init__(
42
+ self,
43
+ url: MPathLike,
44
+ bucket: str = "sentinel-s2-l2a",
45
+ protocol: str = "s3",
46
+ baseline_version: str = "04.00",
47
+ **kwargs,
48
+ ):
49
+ url = MPath.from_inp(url)
50
+ tileinfo_path = url.parent / "tileInfo.json"
51
+ self._path = MPath(
52
+ "/".join(tileinfo_path.elements[-9:-1]), **tileinfo_path._kwargs
53
+ )
54
+ self._utm_zone, self._latitude_band, self._grid_square = self._path.split("/")[
55
+ 1:-4
56
+ ]
57
+ self._baseurl = bucket
58
+ self._protocol = protocol
59
+ self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
60
+
61
+ def product_qi_mask(
62
+ self,
63
+ qi_mask: ProductQI,
64
+ resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
65
+ ) -> MPath:
66
+ """Determine product QI mask according to Sinergise bucket schema."""
67
+ if self.processing_baseline.version < "04.00":
68
+ mask_path = self._PRE_0400_MASK_PATHS[qi_mask]
69
+ else:
70
+ mask_path = self._POST_0400_MASK_PATHS[qi_mask]
71
+ key = f"{self._path}/qi/{mask_path.format(resolution=resolution.name)}"
72
+ return MPath.from_inp(f"{self._protocol}://{self._baseurl}/{key}")
73
+
74
+ def classification_mask(self) -> MPath:
75
+ return self.product_qi_mask(ProductQI.classification)
76
+
77
+ def cloud_probability_mask(
78
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
79
+ ) -> MPath:
80
+ return self.product_qi_mask(ProductQI.cloud_probability, resolution=resolution)
81
+
82
+ def snow_probability_mask(
83
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
84
+ ) -> MPath:
85
+ return self.product_qi_mask(ProductQI.snow_probability, resolution=resolution)
86
+
87
+ def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath:
88
+ """Determine product QI mask according to Sinergise bucket schema."""
89
+ try:
90
+ if self.processing_baseline.version < "04.00":
91
+ mask_path = self._PRE_0400_MASK_PATHS[qi_mask]
92
+ else:
93
+ mask_path = self._POST_0400_MASK_PATHS[qi_mask]
94
+ except KeyError:
95
+ raise DeprecationWarning(
96
+ f"'{qi_mask.name}' quality mask not found in this product"
97
+ )
98
+ key = f"{self._path}/qi/{mask_path.format(band_identifier=band.name)}"
99
+ return MPath.from_inp(f"{self._protocol}://{self._baseurl}/{key}")
100
+
101
+ def technical_quality_mask(self, band: L2ABand) -> MPath:
102
+ return self.band_qi_mask(BandQI.technical_quality, band)
103
+
104
+ def detector_footprints(self, band: L2ABand) -> MPath:
105
+ return self.band_qi_mask(BandQI.detector_footprints, band)
@@ -0,0 +1,26 @@
1
+ import logging
2
+ from typing import Optional, Union
3
+
4
+ import pystac
5
+
6
+ from mapchete_eo.exceptions import CorruptedProductMetadata
7
+ from mapchete_eo.platforms.sentinel2.config import CacheConfig
8
+ from mapchete_eo.platforms.sentinel2.product import S2Product
9
+ from mapchete_eo.product import add_to_blacklist
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def parse_s2_product(
15
+ item: pystac.Item,
16
+ cache_config: Optional[CacheConfig] = None,
17
+ cache_all: bool = False,
18
+ ) -> Union[S2Product, CorruptedProductMetadata]:
19
+ try:
20
+ s2product = S2Product.from_stac_item(
21
+ item, cache_config=cache_config, cache_all=cache_all
22
+ )
23
+ except CorruptedProductMetadata as exc:
24
+ add_to_blacklist(item.get_self_href())
25
+ return exc
26
+ return s2product
@@ -0,0 +1,160 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Optional, Union
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class ProductMaskTypes(BaseModel):
8
+ """Mapping between mask type and respective metadata.xml type key."""
9
+
10
+ classification: Optional[str] = None
11
+ cloud_probability: Optional[str] = None
12
+ snow_probability: Optional[str] = None
13
+
14
+
15
+ class BandMaskTypes(BaseModel):
16
+ """Mapping between band mask type and respective metadata.xml type key."""
17
+
18
+ technical_quality: Optional[str] = None
19
+ detector_footprints: Optional[str] = None
20
+ # deprecated since 04.00
21
+ # nodata: Optional[str] = None
22
+ # defect: Optional[str] = None
23
+ # saturated: Optional[str] = None
24
+
25
+
26
+ class ItemMapping(BaseModel):
27
+ """Configuration of processing baseline keys in metadata.xml."""
28
+
29
+ product_mask_types: ProductMaskTypes
30
+ band_mask_types: BandMaskTypes
31
+ band_mask_extension: str
32
+
33
+
34
+ # Available product mask types from PB 00.01 until 03.01.
35
+ # "classification" mask was provided as GML, the other two as JP2.
36
+ # Cloud probability and snow probability are available in two separate files for
37
+ # 20m and 60m.
38
+ pre_0400 = ItemMapping(
39
+ product_mask_types=ProductMaskTypes(
40
+ classification="MSK_CLOUDS",
41
+ cloud_probability="MSK_CLDPRB",
42
+ snow_probability="MSK_SNWPRB",
43
+ ),
44
+ band_mask_types=BandMaskTypes(
45
+ technical_quality="MSK_TECQUA",
46
+ detector_footprints="MSK_DETFOO",
47
+ ),
48
+ band_mask_extension="gml",
49
+ )
50
+
51
+
52
+ # Available product mask types from PB 04.00 onwards.
53
+ # Cloud probability and snow probability are available in two separate files for
54
+ # 20m and 60m.
55
+ post_0400 = ItemMapping(
56
+ product_mask_types=ProductMaskTypes(
57
+ classification="MSK_CLASSI",
58
+ cloud_probability="MSK_CLDPRB",
59
+ snow_probability="MSK_SNWPRB",
60
+ ),
61
+ band_mask_types=BandMaskTypes(
62
+ technical_quality="MSK_QUALIT",
63
+ detector_footprints="MSK_DETFOO",
64
+ ),
65
+ band_mask_extension="jp2",
66
+ )
67
+
68
+
69
+ @dataclass
70
+ class BaselineVersion:
71
+ """Helper for Processing Baseline versions."""
72
+
73
+ major: int
74
+ minor: int
75
+ level: str
76
+
77
+ @staticmethod
78
+ def from_string(version: str) -> "BaselineVersion":
79
+ major, minor = map(int, version.split("."))
80
+ if major < 2:
81
+ level = "L1C"
82
+ # everything below 02.06 is Level 1C
83
+ elif major == 2 and minor <= 6:
84
+ level = "L1C"
85
+ else:
86
+ level = "L2A"
87
+ return BaselineVersion(major, minor, level)
88
+
89
+ @staticmethod
90
+ def from_inp(inp: Union[str, "BaselineVersion"]) -> "BaselineVersion":
91
+ if isinstance(inp, str):
92
+ return BaselineVersion.from_string(inp)
93
+ elif isinstance(inp, BaselineVersion):
94
+ return inp
95
+ else:
96
+ raise TypeError(f"cannot generate BaselineVersion from input {inp}")
97
+
98
+ def __eq__(self, other: Any):
99
+ other = BaselineVersion.from_inp(other)
100
+ return self.major == other.major and self.minor == other.minor
101
+
102
+ def __lt__(self, other: Union[str, "BaselineVersion"]):
103
+ other = BaselineVersion.from_inp(other)
104
+ if self.major == other.major:
105
+ return self.minor < other.minor
106
+ else:
107
+ return self.major < other.major
108
+
109
+ def __le__(self, other: Union[str, "BaselineVersion"]):
110
+ other = BaselineVersion.from_inp(other)
111
+ if self.major == other.major:
112
+ return self.minor <= other.minor
113
+ else:
114
+ return self.major <= other.major
115
+
116
+ def __gt__(self, other: Union[str, "BaselineVersion"]):
117
+ other = BaselineVersion.from_inp(other)
118
+ if self.major == other.major:
119
+ return self.minor > other.minor
120
+ else:
121
+ return self.major > other.major
122
+
123
+ def __ge__(self, other: Union[str, "BaselineVersion"]):
124
+ if isinstance(other, str):
125
+ other = BaselineVersion.from_string(other)
126
+ if self.major == other.major:
127
+ return self.minor >= other.minor
128
+ else:
129
+ return self.major >= other.major
130
+
131
+ def __str__(self):
132
+ return f"{self.major:02}.{self.minor:02}"
133
+
134
+
135
+ class ProcessingBaseline:
136
+ """Class which combines PB version and metadata.xml keys for QI masks."""
137
+
138
+ version: BaselineVersion
139
+ item_mapping: ItemMapping
140
+ product_mask_types: ProductMaskTypes
141
+ band_mask_types: BandMaskTypes
142
+ band_mask_extension: str
143
+
144
+ def __init__(self, version: BaselineVersion):
145
+ self.version = version
146
+ if self.version.major < 4:
147
+ self.item_mapping = pre_0400
148
+ else:
149
+ self.item_mapping = post_0400
150
+
151
+ self.product_mask_types = self.item_mapping.product_mask_types
152
+ self.band_mask_types = self.item_mapping.band_mask_types
153
+ self.band_mask_extension = self.item_mapping.band_mask_extension
154
+
155
+ @staticmethod
156
+ def from_version(version: Union[BaselineVersion, str]) -> "ProcessingBaseline":
157
+ if isinstance(version, BaselineVersion):
158
+ return ProcessingBaseline(version=version)
159
+ else:
160
+ return ProcessingBaseline(version=BaselineVersion.from_string(version))