mapchete-eo 2025.10.0__py2.py3-none-any.whl → 2025.11.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 (66) hide show
  1. mapchete_eo/__init__.py +1 -1
  2. mapchete_eo/array/convert.py +7 -1
  3. mapchete_eo/base.py +123 -55
  4. mapchete_eo/cli/options_arguments.py +11 -27
  5. mapchete_eo/cli/s2_brdf.py +1 -1
  6. mapchete_eo/cli/s2_cat_results.py +4 -20
  7. mapchete_eo/cli/s2_find_broken_products.py +4 -20
  8. mapchete_eo/cli/s2_jp2_static_catalog.py +2 -2
  9. mapchete_eo/cli/static_catalog.py +4 -45
  10. mapchete_eo/eostac.py +1 -1
  11. mapchete_eo/io/assets.py +7 -7
  12. mapchete_eo/io/items.py +37 -22
  13. mapchete_eo/io/levelled_cubes.py +66 -35
  14. mapchete_eo/io/path.py +19 -8
  15. mapchete_eo/io/products.py +37 -27
  16. mapchete_eo/platforms/sentinel2/__init__.py +1 -1
  17. mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
  18. mapchete_eo/platforms/sentinel2/brdf/correction.py +1 -1
  19. mapchete_eo/platforms/sentinel2/brdf/hls.py +1 -1
  20. mapchete_eo/platforms/sentinel2/brdf/models.py +1 -1
  21. mapchete_eo/platforms/sentinel2/brdf/protocols.py +1 -1
  22. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +1 -1
  23. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +1 -1
  24. mapchete_eo/platforms/sentinel2/config.py +73 -13
  25. mapchete_eo/platforms/sentinel2/driver.py +0 -39
  26. mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
  27. mapchete_eo/platforms/sentinel2/{path_mappers → metadata_parser}/base.py +1 -1
  28. mapchete_eo/platforms/sentinel2/{path_mappers/metadata_xml.py → metadata_parser/default_path_mapper.py} +2 -2
  29. mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
  30. mapchete_eo/platforms/sentinel2/{metadata_parser.py → metadata_parser/s2metadata.py} +51 -146
  31. mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
  32. mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
  33. mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
  34. mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
  35. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +22 -1
  36. mapchete_eo/platforms/sentinel2/processing_baseline.py +3 -0
  37. mapchete_eo/platforms/sentinel2/product.py +88 -23
  38. mapchete_eo/platforms/sentinel2/source.py +114 -0
  39. mapchete_eo/platforms/sentinel2/types.py +5 -0
  40. mapchete_eo/processes/merge_rasters.py +7 -3
  41. mapchete_eo/product.py +14 -9
  42. mapchete_eo/protocols.py +5 -0
  43. mapchete_eo/search/__init__.py +3 -3
  44. mapchete_eo/search/base.py +126 -100
  45. mapchete_eo/search/config.py +25 -4
  46. mapchete_eo/search/s2_mgrs.py +8 -9
  47. mapchete_eo/search/stac_search.py +111 -75
  48. mapchete_eo/search/stac_static.py +63 -94
  49. mapchete_eo/search/utm_search.py +39 -48
  50. mapchete_eo/settings.py +1 -0
  51. mapchete_eo/sort.py +16 -2
  52. mapchete_eo/source.py +107 -0
  53. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/METADATA +2 -1
  54. mapchete_eo-2025.11.0.dist-info/RECORD +89 -0
  55. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/entry_points.txt +1 -1
  56. mapchete_eo/archives/__init__.py +0 -0
  57. mapchete_eo/archives/base.py +0 -65
  58. mapchete_eo/geometry.py +0 -271
  59. mapchete_eo/known_catalogs.py +0 -42
  60. mapchete_eo/platforms/sentinel2/archives.py +0 -190
  61. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +0 -29
  62. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +0 -34
  63. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +0 -105
  64. mapchete_eo-2025.10.0.dist-info/RECORD +0 -88
  65. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/WHEEL +0 -0
  66. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,14 +6,12 @@ sun angles, quality masks, etc.
6
6
  from __future__ import annotations
7
7
 
8
8
  import logging
9
- import warnings
10
9
  from functools import cached_property
11
- from typing import Any, Callable, Dict, List, Optional, Union
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
12
11
  from xml.etree.ElementTree import Element, ParseError
13
12
 
14
13
  import numpy as np
15
14
  import numpy.ma as ma
16
- from pydantic import BaseModel
17
15
  import pystac
18
16
  from affine import Affine
19
17
  from fiona.transform import transform_geom
@@ -33,9 +31,17 @@ from tilematrix import Shape
33
31
 
34
32
  from mapchete_eo.exceptions import AssetEmpty, AssetMissing, CorruptedProductMetadata
35
33
  from mapchete_eo.io import open_xml, read_mask_as_raster
36
- from mapchete_eo.platforms.sentinel2.path_mappers import default_path_mapper_guesser
37
- from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
38
- from mapchete_eo.platforms.sentinel2.path_mappers.metadata_xml import XMLMapper
34
+ from mapchete_eo.io.items import get_item_property
35
+ from mapchete_eo.io.path import asset_mpath
36
+ from mapchete_eo.platforms.sentinel2.metadata_parser.models import (
37
+ ViewingIncidenceAngles,
38
+ SunAngleData,
39
+ SunAnglesData,
40
+ )
41
+ from mapchete_eo.platforms.sentinel2.metadata_parser.base import S2MetadataPathMapper
42
+ from mapchete_eo.platforms.sentinel2.metadata_parser.default_path_mapper import (
43
+ XMLMapper,
44
+ )
39
45
  from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
40
46
  from mapchete_eo.platforms.sentinel2.types import (
41
47
  BandQI,
@@ -59,77 +65,12 @@ def open_granule_metadata_xml(metadata_xml: MPath) -> Element:
59
65
  raise CorruptedProductMetadata(exc)
60
66
 
61
67
 
62
- def s2metadata_from_stac_item(
63
- item: pystac.Item,
64
- metadata_assets: List[str] = ["metadata", "granule_metadata"],
65
- boa_offset_fields: List[str] = [
66
- "sentinel:boa_offset_applied",
67
- "sentinel2:boa_offset_applied",
68
- "earthsearch:boa_offset_applied",
69
- ],
70
- processing_baseline_fields: List[str] = [
71
- "s2:processing_baseline",
72
- "sentinel:processing_baseline",
73
- "sentinel2:processing_baseline",
74
- "processing:version",
75
- ],
76
- **kwargs,
77
- ) -> S2Metadata:
78
- """Custom code to initialize S2Metadata from a STAC item.
79
-
80
- Depending on from which catalog the STAC item comes, this function should correctly
81
- set all custom flags such as BOA offsets or pass on the correct path to the metadata XML
82
- using the proper asset name.
83
- """
84
- metadata_assets = metadata_assets
85
- for metadata_asset in metadata_assets:
86
- if metadata_asset in item.assets:
87
- metadata_path = MPath(item.assets[metadata_asset].href)
88
- break
89
- else: # pragma: no cover
90
- raise KeyError(
91
- f"could not find path to metadata XML file in assets: {', '.join(item.assets.keys())}"
92
- )
93
-
94
- def _determine_offset():
95
- for field in boa_offset_fields:
96
- if item.properties.get(field):
97
- return True
98
-
99
- return False
100
-
101
- boa_offset_applied = _determine_offset()
102
-
103
- if metadata_path.is_remote() or metadata_path.is_absolute():
104
- metadata_xml = metadata_path
105
- else:
106
- metadata_xml = MPath(item.self_href).parent / metadata_path
107
- for processing_baseline_field in processing_baseline_fields:
108
- try:
109
- processing_baseline = item.properties[processing_baseline_field]
110
- break
111
- except KeyError:
112
- pass
113
- else: # pragma: no cover
114
- raise KeyError(
115
- f"could not find processing baseline version in item properties: {item.properties}"
116
- )
117
- return S2Metadata.from_metadata_xml(
118
- metadata_xml=metadata_xml,
119
- processing_baseline=processing_baseline,
120
- boa_offset_applied=boa_offset_applied,
121
- **kwargs,
122
- )
123
-
124
-
125
68
  class S2Metadata:
126
69
  metadata_xml: MPath
127
- path_mapper: S2PathMapper
70
+ path_mapper: S2MetadataPathMapper
128
71
  processing_baseline: ProcessingBaseline
129
72
  boa_offset_applied: bool = False
130
73
  _cached_xml_root: Optional[Element] = None
131
- path_mapper_guesser: Callable = default_path_mapper_guesser
132
- from_stac_item_constructor: Callable = s2metadata_from_stac_item
133
74
  crs: CRS
134
75
  bounds: Bounds
135
76
  footprint: Union[Polygon, MultiPolygon]
@@ -138,7 +79,7 @@ class S2Metadata:
138
79
  def __init__(
139
80
  self,
140
81
  metadata_xml: MPath,
141
- path_mapper: S2PathMapper,
82
+ path_mapper: S2MetadataPathMapper,
142
83
  xml_root: Optional[Element] = None,
143
84
  boa_offset_applied: bool = False,
144
85
  **kwargs,
@@ -161,10 +102,8 @@ class S2Metadata:
161
102
  return f"<S2Metadata id={self.product_id}, processing_baseline={self.processing_baseline}>"
162
103
 
163
104
  def clear_cached_data(self):
164
- logger.debug("clear S2Metadata internal caches")
165
105
  self._cache = dict(viewing_incidence_angles=dict(), detector_footprints=dict())
166
106
  if self._cached_xml_root is not None:
167
- logger.debug("clear S2Metadata xml cache")
168
107
  self._cached_xml_root.clear()
169
108
  self._cached_xml_root = None
170
109
  self.path_mapper.clear_cached_data()
@@ -188,19 +127,15 @@ class S2Metadata:
188
127
  def from_metadata_xml(
189
128
  cls,
190
129
  metadata_xml: Union[str, MPath],
130
+ path_mapper: Optional[S2MetadataPathMapper] = None,
191
131
  processing_baseline: Optional[str] = None,
192
- path_mapper: Optional[S2PathMapper] = None,
193
132
  **kwargs,
194
133
  ) -> S2Metadata:
195
134
  metadata_xml = MPath.from_inp(metadata_xml, **kwargs)
196
135
  xml_root = open_granule_metadata_xml(metadata_xml)
136
+
197
137
  if path_mapper is None:
198
- # guess correct path mapper
199
- path_mapper = cls.path_mapper_guesser(
200
- metadata_xml,
201
- xml_root=xml_root,
202
- **kwargs,
203
- )
138
+ path_mapper = XMLMapper(metadata_xml=metadata_xml, xml_root=xml_root)
204
139
 
205
140
  # use processing baseline version from argument if available
206
141
  if processing_baseline:
@@ -219,9 +154,38 @@ class S2Metadata:
219
154
  metadata_xml, path_mapper=path_mapper, xml_root=xml_root, **kwargs
220
155
  )
221
156
 
222
- @classmethod
223
- def from_stac_item(cls, item: pystac.Item, **kwargs) -> S2Metadata:
224
- return cls.from_stac_item_constructor(item, **kwargs)
157
+ @staticmethod
158
+ def from_stac_item(
159
+ item: pystac.Item,
160
+ metadata_xml_asset_name: Tuple[str, ...] = ("metadata", "granule_metadata"),
161
+ boa_offset_field: Union[str, Tuple[str, ...]] = (
162
+ "earthsearch:boa_offset_applied"
163
+ ),
164
+ processing_baseline_field: Union[str, Tuple[str, ...]] = (
165
+ "s2:processing_baseline",
166
+ "sentinel2:processing_baseline",
167
+ "processing:version",
168
+ ),
169
+ **kwargs,
170
+ ) -> S2Metadata:
171
+ # try to find path to metadata.xml
172
+ metadata_xml_path = asset_mpath(item, metadata_xml_asset_name)
173
+ # make path absolute
174
+ if not (metadata_xml_path.is_remote() or metadata_xml_path.is_absolute()):
175
+ metadata_xml_path = MPath(item.self_href).parent / metadata_xml_path
176
+
177
+ # try to find information on processing baseline version
178
+ processing_baseline = get_item_property(item, processing_baseline_field)
179
+
180
+ # see if boa_offset_applied flag is available
181
+ boa_offset_applied = get_item_property(item, boa_offset_field, default=False)
182
+
183
+ return S2Metadata.from_metadata_xml(
184
+ metadata_xml=metadata_xml_path,
185
+ processing_baseline=processing_baseline,
186
+ boa_offset_applied=boa_offset_applied,
187
+ **kwargs,
188
+ )
225
189
 
226
190
  @property
227
191
  def xml_root(self):
@@ -272,13 +236,13 @@ class S2Metadata:
272
236
  for product_qi_mask in ProductQI:
273
237
  if product_qi_mask == ProductQI.classification:
274
238
  out[product_qi_mask.name] = self.path_mapper.product_qi_mask(
275
- product_qi_mask
239
+ qi_mask=product_qi_mask
276
240
  )
277
241
  else:
278
242
  for resolution in ProductQIMaskResolution:
279
243
  out[f"{product_qi_mask.name}-{resolution.name}"] = (
280
244
  self.path_mapper.product_qi_mask(
281
- product_qi_mask, resolution=resolution
245
+ qi_mask=product_qi_mask, resolution=resolution
282
246
  )
283
247
  )
284
248
 
@@ -589,65 +553,6 @@ class S2Metadata:
589
553
  return mean
590
554
 
591
555
 
592
- class SunAngleData(BaseModel):
593
- model_config = dict(arbitrary_types_allowed=True)
594
- raster: ReferencedRaster
595
- mean: float
596
-
597
-
598
- class SunAnglesData(BaseModel):
599
- azimuth: SunAngleData
600
- zenith: SunAngleData
601
-
602
- def get_angle(self, angle: SunAngle) -> SunAngleData:
603
- if angle == SunAngle.azimuth:
604
- return self.azimuth
605
- elif angle == SunAngle.zenith:
606
- return self.zenith
607
- else:
608
- raise KeyError(f"unknown angle: {angle}")
609
-
610
-
611
- class ViewingIncidenceAngle(BaseModel):
612
- model_config = dict(arbitrary_types_allowed=True)
613
- detectors: Dict[int, ReferencedRaster]
614
- mean: float
615
-
616
- def merge_detectors(
617
- self, fill_edges: bool = True, smoothing_iterations: int = 3
618
- ) -> ReferencedRaster:
619
- if not self.detectors:
620
- raise CorruptedProductMetadata("no viewing incidence angles available")
621
- sample = next(iter(self.detectors.values()))
622
- with warnings.catch_warnings():
623
- warnings.simplefilter("ignore", category=RuntimeWarning)
624
- merged = np.nanmean(
625
- np.stack([raster.data for raster in self.detectors.values()]), axis=0
626
- )
627
- if fill_edges:
628
- merged = fillnodata(
629
- ma.masked_invalid(merged), smoothing_iterations=smoothing_iterations
630
- )
631
- return ReferencedRaster.from_array_like(
632
- array_like=ma.masked_invalid(merged),
633
- transform=sample.transform,
634
- crs=sample.crs,
635
- )
636
-
637
-
638
- class ViewingIncidenceAngles(BaseModel):
639
- azimuth: ViewingIncidenceAngle
640
- zenith: ViewingIncidenceAngle
641
-
642
- def get_angle(self, angle: ViewAngle) -> ViewingIncidenceAngle:
643
- if angle == ViewAngle.azimuth:
644
- return self.azimuth
645
- elif angle == ViewAngle.zenith:
646
- return self.zenith
647
- else:
648
- raise KeyError(f"unknown angle: {angle}")
649
-
650
-
651
556
  def _get_grids(root: Element, crs: CRS) -> Dict[Resolution, Grid]:
652
557
  geoinfo = {
653
558
  Resolution["10m"]: dict(crs=crs),
@@ -0,0 +1,57 @@
1
+ from typing import Dict, Any
2
+
3
+ # importing this is crucial so the mapping functions get registered before registry is accessed
4
+ from mapchete_eo.platforms.sentinel2.preconfigured_sources.item_mappers import (
5
+ earthsearch_assets_paths_mapper,
6
+ earthsearch_id_mapper,
7
+ earthsearch_to_s2metadata,
8
+ cdse_asset_names,
9
+ cdse_s2metadata,
10
+ )
11
+ from mapchete_eo.platforms.sentinel2.preconfigured_sources.guessers import (
12
+ guess_metadata_path_mapper,
13
+ guess_s2metadata_from_item,
14
+ guess_s2metadata_from_metadata_xml,
15
+ )
16
+
17
+
18
+ __all__ = [
19
+ "guess_metadata_path_mapper",
20
+ "guess_s2metadata_from_item",
21
+ "guess_s2metadata_from_metadata_xml",
22
+ "earthsearch_assets_paths_mapper",
23
+ "earthsearch_id_mapper",
24
+ "earthsearch_to_s2metadata",
25
+ "cdse_asset_names",
26
+ "cdse_s2metadata",
27
+ ]
28
+
29
+ KNOWN_SOURCES: Dict[str, Any] = {
30
+ "EarthSearch": {
31
+ "collection": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a",
32
+ },
33
+ "EarthSearch_legacy": {
34
+ "collection": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a",
35
+ },
36
+ "CDSE": {
37
+ "collection": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l2a",
38
+ "metadata_archive": "CDSE",
39
+ },
40
+ }
41
+
42
+ DEPRECATED_ARCHIVES = {
43
+ "S2AWS_COG": {
44
+ "collection": "https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a",
45
+ },
46
+ "S2AWS_JP2": {
47
+ "collection": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l2a",
48
+ "data_archive": "AWSJP2",
49
+ },
50
+ "S2CDSE_AWSJP2": {
51
+ "collection": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l2a",
52
+ "data_archive": "AWSJP2",
53
+ },
54
+ "S2CDSE_JP2": {
55
+ "collection": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l2a",
56
+ },
57
+ }
@@ -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
+ )