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.
- mapchete_eo/__init__.py +1 -0
- mapchete_eo/array/__init__.py +0 -0
- mapchete_eo/array/buffer.py +16 -0
- mapchete_eo/array/color.py +29 -0
- mapchete_eo/array/convert.py +163 -0
- mapchete_eo/base.py +653 -0
- mapchete_eo/blacklist.txt +175 -0
- mapchete_eo/cli/__init__.py +30 -0
- mapchete_eo/cli/bounds.py +22 -0
- mapchete_eo/cli/options_arguments.py +227 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +130 -0
- mapchete_eo/cli/s2_find_broken_products.py +77 -0
- mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
- mapchete_eo/cli/s2_mask.py +71 -0
- mapchete_eo/cli/s2_mgrs.py +45 -0
- mapchete_eo/cli/s2_rgb.py +114 -0
- mapchete_eo/cli/s2_verify.py +129 -0
- mapchete_eo/cli/static_catalog.py +82 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/blend_functions.py +579 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +266 -0
- mapchete_eo/image_operations/dtype_scale.py +43 -0
- mapchete_eo/image_operations/fillnodata.py +130 -0
- mapchete_eo/image_operations/filters.py +319 -0
- mapchete_eo/image_operations/linear_normalization.py +81 -0
- mapchete_eo/image_operations/sigmoidal.py +114 -0
- mapchete_eo/io/__init__.py +37 -0
- mapchete_eo/io/assets.py +496 -0
- mapchete_eo/io/items.py +162 -0
- mapchete_eo/io/levelled_cubes.py +259 -0
- mapchete_eo/io/path.py +155 -0
- mapchete_eo/io/products.py +423 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
- mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
- mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
- mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
- mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
- mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
- mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
- mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
- mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
- mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
- mapchete_eo/platforms/sentinel2/config.py +241 -0
- mapchete_eo/platforms/sentinel2/driver.py +43 -0
- mapchete_eo/platforms/sentinel2/masks.py +329 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/base.py +56 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py +135 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
- mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py +639 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
- mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +50 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +163 -0
- mapchete_eo/platforms/sentinel2/product.py +747 -0
- mapchete_eo/platforms/sentinel2/source.py +114 -0
- mapchete_eo/platforms/sentinel2/types.py +114 -0
- mapchete_eo/processes/__init__.py +0 -0
- mapchete_eo/processes/config.py +51 -0
- mapchete_eo/processes/dtype_scale.py +112 -0
- mapchete_eo/processes/eo_to_xarray.py +19 -0
- mapchete_eo/processes/merge_rasters.py +239 -0
- mapchete_eo/product.py +323 -0
- mapchete_eo/protocols.py +61 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +285 -0
- mapchete_eo/search/config.py +113 -0
- mapchete_eo/search/s2_mgrs.py +313 -0
- mapchete_eo/search/stac_search.py +278 -0
- mapchete_eo/search/stac_static.py +197 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +25 -0
- mapchete_eo/sort.py +60 -0
- mapchete_eo/source.py +109 -0
- mapchete_eo/time.py +62 -0
- mapchete_eo/types.py +76 -0
- mapchete_eo-2026.2.0.dist-info/METADATA +91 -0
- mapchete_eo-2026.2.0.dist-info/RECORD +89 -0
- mapchete_eo-2026.2.0.dist-info/WHEEL +4 -0
- mapchete_eo-2026.2.0.dist-info/entry_points.txt +11 -0
- 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
|