mapchete-eo 2026.2.0__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 (89) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/array/__init__.py +0 -0
  3. mapchete_eo/array/buffer.py +16 -0
  4. mapchete_eo/array/color.py +29 -0
  5. mapchete_eo/array/convert.py +163 -0
  6. mapchete_eo/base.py +653 -0
  7. mapchete_eo/blacklist.txt +175 -0
  8. mapchete_eo/cli/__init__.py +30 -0
  9. mapchete_eo/cli/bounds.py +22 -0
  10. mapchete_eo/cli/options_arguments.py +227 -0
  11. mapchete_eo/cli/s2_brdf.py +77 -0
  12. mapchete_eo/cli/s2_cat_results.py +130 -0
  13. mapchete_eo/cli/s2_find_broken_products.py +77 -0
  14. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  15. mapchete_eo/cli/s2_mask.py +71 -0
  16. mapchete_eo/cli/s2_mgrs.py +45 -0
  17. mapchete_eo/cli/s2_rgb.py +114 -0
  18. mapchete_eo/cli/s2_verify.py +129 -0
  19. mapchete_eo/cli/static_catalog.py +82 -0
  20. mapchete_eo/eostac.py +30 -0
  21. mapchete_eo/exceptions.py +87 -0
  22. mapchete_eo/image_operations/__init__.py +12 -0
  23. mapchete_eo/image_operations/blend_functions.py +579 -0
  24. mapchete_eo/image_operations/color_correction.py +136 -0
  25. mapchete_eo/image_operations/compositing.py +266 -0
  26. mapchete_eo/image_operations/dtype_scale.py +43 -0
  27. mapchete_eo/image_operations/fillnodata.py +130 -0
  28. mapchete_eo/image_operations/filters.py +319 -0
  29. mapchete_eo/image_operations/linear_normalization.py +81 -0
  30. mapchete_eo/image_operations/sigmoidal.py +114 -0
  31. mapchete_eo/io/__init__.py +37 -0
  32. mapchete_eo/io/assets.py +496 -0
  33. mapchete_eo/io/items.py +162 -0
  34. mapchete_eo/io/levelled_cubes.py +259 -0
  35. mapchete_eo/io/path.py +155 -0
  36. mapchete_eo/io/products.py +423 -0
  37. mapchete_eo/io/profiles.py +45 -0
  38. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  39. mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
  40. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  41. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  42. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  43. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  44. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  45. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  46. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  47. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  48. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  49. mapchete_eo/platforms/sentinel2/config.py +241 -0
  50. mapchete_eo/platforms/sentinel2/driver.py +43 -0
  51. mapchete_eo/platforms/sentinel2/masks.py +329 -0
  52. mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
  53. mapchete_eo/platforms/sentinel2/metadata_parser/base.py +56 -0
  54. mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py +135 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
  56. mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py +639 -0
  57. mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
  58. mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
  59. mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
  60. mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +50 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +163 -0
  63. mapchete_eo/platforms/sentinel2/product.py +747 -0
  64. mapchete_eo/platforms/sentinel2/source.py +114 -0
  65. mapchete_eo/platforms/sentinel2/types.py +114 -0
  66. mapchete_eo/processes/__init__.py +0 -0
  67. mapchete_eo/processes/config.py +51 -0
  68. mapchete_eo/processes/dtype_scale.py +112 -0
  69. mapchete_eo/processes/eo_to_xarray.py +19 -0
  70. mapchete_eo/processes/merge_rasters.py +239 -0
  71. mapchete_eo/product.py +323 -0
  72. mapchete_eo/protocols.py +61 -0
  73. mapchete_eo/search/__init__.py +14 -0
  74. mapchete_eo/search/base.py +285 -0
  75. mapchete_eo/search/config.py +113 -0
  76. mapchete_eo/search/s2_mgrs.py +313 -0
  77. mapchete_eo/search/stac_search.py +278 -0
  78. mapchete_eo/search/stac_static.py +197 -0
  79. mapchete_eo/search/utm_search.py +251 -0
  80. mapchete_eo/settings.py +25 -0
  81. mapchete_eo/sort.py +60 -0
  82. mapchete_eo/source.py +109 -0
  83. mapchete_eo/time.py +62 -0
  84. mapchete_eo/types.py +76 -0
  85. mapchete_eo-2026.2.0.dist-info/METADATA +91 -0
  86. mapchete_eo-2026.2.0.dist-info/RECORD +89 -0
  87. mapchete_eo-2026.2.0.dist-info/WHEEL +4 -0
  88. mapchete_eo-2026.2.0.dist-info/entry_points.txt +11 -0
  89. mapchete_eo-2026.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,108 @@
1
+ from typing import List
2
+
3
+ from mapchete.path import MPathLike, MPath
4
+ from pystac import Item
5
+
6
+ from mapchete_eo.platforms.sentinel2.metadata_parser.base import S2MetadataPathMapper
7
+ from mapchete_eo.platforms.sentinel2.metadata_parser.default_path_mapper import (
8
+ XMLMapper,
9
+ )
10
+ from mapchete_eo.platforms.sentinel2.metadata_parser.s2metadata import S2Metadata
11
+ from mapchete_eo.platforms.sentinel2.preconfigured_sources.metadata_xml_mappers import (
12
+ EarthSearchPathMapper,
13
+ SinergisePathMapper,
14
+ )
15
+
16
+
17
+ def guess_metadata_path_mapper(
18
+ metadata_xml: MPathLike, **kwargs
19
+ ) -> S2MetadataPathMapper:
20
+ """Guess S2PathMapper based on URL.
21
+
22
+ If a new path mapper is added in this module, it should also be added to this function
23
+ in order to be detected.
24
+ """
25
+ metadata_xml = MPath.from_inp(metadata_xml)
26
+ if metadata_xml.startswith(
27
+ ("https://roda.sentinel-hub.com/sentinel-s2-l2a/", "s3://sentinel-s2-l2a/")
28
+ ) or metadata_xml.startswith(
29
+ ("https://roda.sentinel-hub.com/sentinel-s2-l1c/", "s3://sentinel-s2-l1c/")
30
+ ):
31
+ return SinergisePathMapper(metadata_xml, **kwargs)
32
+ elif metadata_xml.startswith(
33
+ "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/"
34
+ ):
35
+ return EarthSearchPathMapper(metadata_xml, **kwargs)
36
+ else:
37
+ return XMLMapper(metadata_xml, **kwargs)
38
+
39
+
40
+ def guess_s2metadata_from_metadata_xml(metadata_xml: MPathLike, **kwargs) -> S2Metadata:
41
+ return S2Metadata.from_metadata_xml(
42
+ metadata_xml=metadata_xml,
43
+ path_mapper=guess_metadata_path_mapper(metadata_xml, **kwargs),
44
+ **kwargs,
45
+ )
46
+
47
+
48
+ def guess_s2metadata_from_item(
49
+ item: Item,
50
+ metadata_assets: List[str] = ["metadata", "granule_metadata"],
51
+ boa_offset_fields: List[str] = [
52
+ "sentinel:boa_offset_applied",
53
+ "sentinel2:boa_offset_applied",
54
+ "earthsearch:boa_offset_applied",
55
+ ],
56
+ processing_baseline_fields: List[str] = [
57
+ "s2:processing_baseline",
58
+ "sentinel:processing_baseline",
59
+ "sentinel2:processing_baseline",
60
+ "processing:version",
61
+ ],
62
+ **kwargs,
63
+ ) -> S2Metadata:
64
+ """Custom code to initialize S2Metadata from a STAC item.
65
+
66
+ Depending on from which catalog the STAC item comes, this function should correctly
67
+ set all custom flags such as BOA offsets or pass on the correct path to the metadata XML
68
+ using the proper asset name.
69
+ """
70
+ metadata_assets = metadata_assets
71
+ for metadata_asset in metadata_assets:
72
+ if metadata_asset in item.assets:
73
+ metadata_path = MPath(item.assets[metadata_asset].href)
74
+ break
75
+ else: # pragma: no cover
76
+ raise KeyError(
77
+ f"could not find path to metadata XML file in assets: {', '.join(item.assets.keys())}"
78
+ )
79
+
80
+ def _determine_offset():
81
+ for field in boa_offset_fields:
82
+ if item.properties.get(field):
83
+ return True
84
+
85
+ return False
86
+
87
+ boa_offset_applied = _determine_offset()
88
+
89
+ if metadata_path.is_remote() or metadata_path.is_absolute():
90
+ metadata_xml = metadata_path
91
+ else:
92
+ metadata_xml = MPath(item.self_href).parent / metadata_path
93
+ for processing_baseline_field in processing_baseline_fields:
94
+ try:
95
+ processing_baseline = item.properties[processing_baseline_field]
96
+ break
97
+ except KeyError:
98
+ pass
99
+ else: # pragma: no cover
100
+ raise KeyError(
101
+ f"could not find processing baseline version in item properties: {item.properties}"
102
+ )
103
+ return guess_s2metadata_from_metadata_xml(
104
+ metadata_xml,
105
+ processing_baseline=processing_baseline,
106
+ boa_offset_applied=boa_offset_applied,
107
+ **kwargs,
108
+ )
@@ -0,0 +1,171 @@
1
+ from mapchete.path import MPath
2
+ from pystac import Item
3
+
4
+ from mapchete_eo.platforms.sentinel2._mapper_registry import (
5
+ maps_item_id,
6
+ maps_stac_metadata,
7
+ creates_s2metadata,
8
+ )
9
+ from mapchete_eo.platforms.sentinel2.preconfigured_sources.metadata_xml_mappers import (
10
+ CDSEPathMapper,
11
+ EarthSearchPathMapper,
12
+ EarthSearchC1PathMapper,
13
+ SinergisePathMapper,
14
+ )
15
+ from mapchete_eo.platforms.sentinel2.metadata_parser.s2metadata import S2Metadata
16
+ from mapchete_eo.search.s2_mgrs import S2Tile
17
+
18
+
19
+ # mapper functions decorated with metadata to have driver decide which one to apply when #
20
+ ##########################################################################################
21
+
22
+
23
+ @maps_item_id(from_collections=["EarthSearch", "EarthSearch_legacy"])
24
+ def earthsearch_id_mapper(item: Item) -> Item:
25
+ item.id = item.properties["s2:product_uri"].rstrip(".SAFE")
26
+ return item
27
+
28
+
29
+ @maps_stac_metadata(from_collections=["EarthSearch"], to_data_archives=["AWSCOG"])
30
+ def earthsearch_assets_paths_mapper(item: Item) -> Item:
31
+ """Nothing to do here as paths match catalog."""
32
+ return item
33
+
34
+
35
+ @creates_s2metadata(from_collections=["EarthSearch"], to_metadata_archives=["roda"])
36
+ def earthsearch_to_s2metadata(item: Item) -> S2Metadata:
37
+ return S2Metadata.from_stac_item(
38
+ item,
39
+ path_mapper=EarthSearchC1PathMapper(
40
+ MPath(item.assets["granule_metadata"].href)
41
+ ),
42
+ processing_baseline_field="s2:processing_baseline",
43
+ )
44
+
45
+
46
+ @creates_s2metadata(
47
+ from_collections=["EarthSearch_legacy"], to_metadata_archives=["roda"]
48
+ )
49
+ def earthsearch_legacy_to_s2metadata(item: Item) -> S2Metadata:
50
+ return S2Metadata.from_stac_item(
51
+ item,
52
+ path_mapper=EarthSearchPathMapper(MPath(item.assets["granule_metadata"].href)),
53
+ boa_offset_field="earthsearch:boa_offset_applied",
54
+ processing_baseline_field="s2:processing_baseline",
55
+ )
56
+
57
+
58
+ @maps_item_id(from_collections=["CDSE"])
59
+ def plain_id_mapper(item: Item) -> Item:
60
+ return item
61
+
62
+
63
+ CDSE_ASSET_NAME_MAPPING = {
64
+ "AOT_10m": "aot",
65
+ "B01_20m": "coastal",
66
+ "B02_10m": "blue",
67
+ "B03_10m": "green",
68
+ "B04_10m": "red",
69
+ "B05_20m": "rededge1",
70
+ "B06_20m": "rededge2",
71
+ "B07_20m": "rededge3",
72
+ "B08_10m": "nir",
73
+ "B09_60m": "nir09",
74
+ "B11_20m": "swir16",
75
+ "B12_20m": "swir22",
76
+ "B8A_20m": "nir08",
77
+ "SCL_20m": "scl",
78
+ "TCI_10m": "visual",
79
+ "WVP_10m": "wvp",
80
+ }
81
+
82
+
83
+ @maps_stac_metadata(from_collections=["CDSE"])
84
+ def cdse_asset_names(item: Item) -> Item:
85
+ new_assets = {}
86
+ for asset_name, asset in item.assets.items():
87
+ if asset_name in CDSE_ASSET_NAME_MAPPING:
88
+ asset_name = CDSE_ASSET_NAME_MAPPING[asset_name]
89
+ new_assets[asset_name] = asset
90
+
91
+ item.assets = new_assets
92
+
93
+ item.properties["s2:datastrip_id"] = item.properties.get("eopf:datastrip_id")
94
+ return item
95
+
96
+
97
+ @maps_stac_metadata(from_collections=["CDSE"], to_data_archives=["AWSJP2"])
98
+ def map_cdse_paths_to_jp2_archive(item: Item) -> Item:
99
+ """
100
+ CSDE has the following assets:
101
+ AOT_10m, AOT_20m, AOT_60m, B01_20m, B01_60m, B02_10m, B02_20m, B02_60m, B03_10m, B03_20m,
102
+ B03_60m, B04_10m, B04_20m, B04_60m, B05_20m, B05_60m, B06_20m, B06_60m, B07_20m, B07_60m,
103
+ B08_10m, B09_60m, B11_20m, B11_60m, B12_20m, B12_60m, B8A_20m, B8A_60m, Product, SCL_20m,
104
+ SCL_60m, TCI_10m, TCI_20m, TCI_60m, WVP_10m, WVP_20m, WVP_60m, thumbnail, safe_manifest,
105
+ granule_metadata, inspire_metadata, product_metadata, datastrip_metadata
106
+
107
+ sample path for AWS JP2:
108
+ s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/R10m/
109
+ """
110
+ if item.datetime is None:
111
+ raise ValueError(f"product {item.get_self_href()} does not have a timestamp")
112
+ path_base_scheme = "s3://sentinel-s2-l2a/tiles/{utm_zone}/{latitude_band}/{grid_square}/{year}/{month}/{day}/{count}"
113
+ s2tile = S2Tile.from_grid_code(item.properties["grid:code"])
114
+ product_basepath = MPath(
115
+ path_base_scheme.format(
116
+ utm_zone=int(s2tile.utm_zone),
117
+ latitude_band=s2tile.latitude_band,
118
+ grid_square=s2tile.grid_square,
119
+ year=item.datetime.year,
120
+ month=item.datetime.month,
121
+ day=item.datetime.day,
122
+ count=0, # TODO: get count dynamically from metadata
123
+ )
124
+ )
125
+ new_assets = {}
126
+ for asset_name, asset in item.assets.items():
127
+ # ignore these assets
128
+ if asset_name in [
129
+ "Product",
130
+ "safe_manifest",
131
+ "product_metadata",
132
+ "inspire_metadata",
133
+ "datastrip_metadata",
134
+ ]:
135
+ continue
136
+ # set thumbnnail
137
+ elif asset_name == "thumbnail":
138
+ asset.href = str(product_basepath / "R60m" / "TCI.jp2")
139
+ # point to proper metadata
140
+ elif asset_name == "granule_metadata":
141
+ asset.href = str(product_basepath / "metadata.xml")
142
+ # change band asset names and point to their new locations
143
+ elif asset_name in CDSE_ASSET_NAME_MAPPING:
144
+ name, resolution = asset_name.split("_")
145
+ asset.href = product_basepath / f"R{resolution}" / f"{name}.jp2"
146
+ asset_name = CDSE_ASSET_NAME_MAPPING[asset_name]
147
+ else:
148
+ continue
149
+ new_assets[asset_name] = asset
150
+
151
+ item.assets = new_assets
152
+
153
+ return item
154
+
155
+
156
+ @creates_s2metadata(from_collections=["CDSE"], to_metadata_archives=["CDSE"])
157
+ def cdse_s2metadata(item: Item) -> S2Metadata:
158
+ return S2Metadata.from_stac_item(
159
+ item,
160
+ path_mapper=CDSEPathMapper(MPath(item.assets["granule_metadata"].href)),
161
+ processing_baseline_field="processing:version",
162
+ )
163
+
164
+
165
+ @creates_s2metadata(from_collections=["CDSE"], to_metadata_archives=["roda"])
166
+ def cdse_to_roda_s2metadata(item: Item) -> S2Metadata:
167
+ return S2Metadata.from_stac_item(
168
+ item,
169
+ path_mapper=SinergisePathMapper(MPath(item.assets["granule_metadata"].href)),
170
+ processing_baseline_field="processing:version",
171
+ )
@@ -0,0 +1,217 @@
1
+ from mapchete.path import MPath, MPathLike
2
+
3
+ from mapchete_eo.platforms.sentinel2.metadata_parser.base import S2MetadataPathMapper
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(S2MetadataPathMapper):
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)
106
+
107
+
108
+ class EarthSearchPathMapper(SinergisePathMapper):
109
+ """
110
+ The COG archive maintained by E84 and covered by EarthSearch does not hold additional data
111
+ such as the GML files. This class maps the metadata masks to the current EarthSearch product.
112
+
113
+ e.g.:
114
+ B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_DETFOO_B01.gml
115
+ Cloud masks: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_CLOUDS_B00.gml
116
+
117
+ newer products however:
118
+ B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2022/6/6/0/qi/DETFOO_B01.jp2
119
+ no vector cloudmasks available anymore
120
+ """
121
+
122
+ def __init__(
123
+ self,
124
+ metadata_xml: MPath,
125
+ alternative_metadata_baseurl: str = "sentinel-s2-l2a",
126
+ protocol: str = "s3",
127
+ baseline_version: str = "04.00",
128
+ **kwargs,
129
+ ):
130
+ basedir = metadata_xml.parent
131
+ self._path = (basedir / "tileinfo_metadata.json").read_json()["path"]
132
+ self._utm_zone, self._latitude_band, self._grid_square = basedir.elements[-6:-3]
133
+ self._baseurl = alternative_metadata_baseurl
134
+ self._protocol = protocol
135
+ self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
136
+
137
+
138
+ class EarthSearchC1PathMapper(SinergisePathMapper):
139
+ """
140
+ The newer C1 collection has cloud and snow probability masks as assets, so we only need to
141
+ map to the rest.
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ metadata_xml: MPath,
147
+ alternative_metadata_baseurl: str = "sentinel-s2-l2a",
148
+ protocol: str = "s3",
149
+ baseline_version: str = "04.00",
150
+ **kwargs,
151
+ ):
152
+ basedir = metadata_xml.parent
153
+ self._path = (basedir / "tileInfo.json").read_json()["path"]
154
+ self._utm_zone, self._latitude_band, self._grid_square = basedir.elements[-6:-3]
155
+ self._baseurl = alternative_metadata_baseurl
156
+ self._protocol = protocol
157
+ self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
158
+
159
+
160
+ class CDSEPathMapper(S2MetadataPathMapper):
161
+ _MASK_FILENAMES = {
162
+ ProductQI.classification: "MSK_CLASSI_B00.jp2",
163
+ ProductQI.cloud_probability: "MSK_CLDPRB_{resolution}.jp2",
164
+ ProductQI.snow_probability: "MSK_SNWPRB_{resolution}.jp2",
165
+ BandQI.detector_footprints: "MSK_DETFOO_{band_identifier}.jp2",
166
+ BandQI.technical_quality: "MSK_QUALIT_{band_identifier}.jp2",
167
+ }
168
+
169
+ def __init__(
170
+ self,
171
+ url: MPathLike,
172
+ baseline_version: str = "04.00",
173
+ **kwargs,
174
+ ):
175
+ url = MPath.from_inp(url)
176
+ self._path = url.parent
177
+ self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
178
+
179
+ def product_qi_mask(
180
+ self,
181
+ qi_mask: ProductQI,
182
+ resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
183
+ ) -> MPath:
184
+ """Determine product QI mask according to Sinergise bucket schema."""
185
+ mask_path = self._MASK_FILENAMES[qi_mask]
186
+ key = f"QI_DATA/{mask_path.format(resolution=resolution.name)}"
187
+ return self._path / key
188
+
189
+ def classification_mask(self) -> MPath:
190
+ return self.product_qi_mask(ProductQI.classification)
191
+
192
+ def cloud_probability_mask(
193
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
194
+ ) -> MPath:
195
+ return self.product_qi_mask(ProductQI.cloud_probability, resolution=resolution)
196
+
197
+ def snow_probability_mask(
198
+ self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
199
+ ) -> MPath:
200
+ return self.product_qi_mask(ProductQI.snow_probability, resolution=resolution)
201
+
202
+ def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath:
203
+ """Determine product QI mask according to Sinergise bucket schema."""
204
+ try:
205
+ mask_path = self._MASK_FILENAMES[qi_mask]
206
+ except KeyError:
207
+ raise DeprecationWarning(
208
+ f"'{qi_mask.name}' quality mask not found in this product"
209
+ )
210
+ key = f"QI_DATA/{mask_path.format(band_identifier=band.name)}"
211
+ return self._path / key
212
+
213
+ def technical_quality_mask(self, band: L2ABand) -> MPath:
214
+ return self.band_qi_mask(BandQI.technical_quality, band)
215
+
216
+ def detector_footprints(self, band: L2ABand) -> MPath:
217
+ return self.band_qi_mask(BandQI.detector_footprints, band)
@@ -0,0 +1,50 @@
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.io.items import get_item_property
8
+ from mapchete_eo.platforms.sentinel2.config import CacheConfig
9
+ from mapchete_eo.platforms.sentinel2.product import S2Product
10
+ from mapchete_eo.platforms.sentinel2.source import Sentinel2Source
11
+ from mapchete_eo.product import add_to_blacklist
12
+ from mapchete_eo.settings import mapchete_eo_settings
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def parse_s2_product(
18
+ item: pystac.Item,
19
+ cache_config: Optional[CacheConfig] = None,
20
+ cache_all: bool = False,
21
+ ) -> Union[S2Product, CorruptedProductMetadata]:
22
+ """
23
+ Parse a Sentinel-2 STAC Item into an S2Product.
24
+ """
25
+ # use mapper from source if applicable
26
+ source: Union[Sentinel2Source, None] = item.properties.pop(
27
+ "mapchete_eo:source", None
28
+ )
29
+ try:
30
+ s2product = S2Product.from_stac_item(
31
+ item,
32
+ cache_config=cache_config,
33
+ cache_all=cache_all,
34
+ metadata_mapper=None if source is None else source.get_s2metadata_mapper(),
35
+ item_modifier_funcs=None if source is None else source.item_modifier_funcs,
36
+ lazy_load_item=mapchete_eo_settings.lazy_load_stac_items,
37
+ item_property_cache={
38
+ key: get_item_property(item, key)
39
+ for key in [
40
+ "datetime",
41
+ "eo:cloud_cover",
42
+ "id",
43
+ "s2:datastrip_id",
44
+ ]
45
+ },
46
+ )
47
+ except CorruptedProductMetadata as exc:
48
+ add_to_blacklist(item.get_self_href())
49
+ return exc
50
+ return s2product