mapchete-eo 2025.10.1__py2.py3-none-any.whl → 2026.1.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.
- mapchete_eo/__init__.py +1 -1
- mapchete_eo/base.py +94 -54
- mapchete_eo/cli/options_arguments.py +11 -27
- mapchete_eo/cli/s2_brdf.py +1 -1
- mapchete_eo/cli/s2_cat_results.py +4 -20
- mapchete_eo/cli/s2_find_broken_products.py +4 -20
- mapchete_eo/cli/s2_jp2_static_catalog.py +2 -2
- mapchete_eo/cli/static_catalog.py +4 -45
- mapchete_eo/eostac.py +1 -1
- mapchete_eo/io/assets.py +20 -16
- mapchete_eo/io/items.py +36 -23
- mapchete_eo/io/path.py +19 -8
- mapchete_eo/io/products.py +22 -24
- mapchete_eo/platforms/sentinel2/__init__.py +1 -1
- mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
- mapchete_eo/platforms/sentinel2/brdf/correction.py +1 -1
- mapchete_eo/platforms/sentinel2/brdf/hls.py +1 -1
- mapchete_eo/platforms/sentinel2/brdf/models.py +1 -1
- mapchete_eo/platforms/sentinel2/brdf/protocols.py +1 -1
- mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +1 -1
- mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +1 -1
- mapchete_eo/platforms/sentinel2/config.py +73 -13
- mapchete_eo/platforms/sentinel2/driver.py +0 -39
- mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
- mapchete_eo/platforms/sentinel2/{path_mappers → metadata_parser}/base.py +1 -1
- mapchete_eo/platforms/sentinel2/{path_mappers/metadata_xml.py → metadata_parser/default_path_mapper.py} +2 -2
- mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
- mapchete_eo/platforms/sentinel2/{metadata_parser.py → metadata_parser/s2metadata.py} +51 -144
- 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 +22 -1
- mapchete_eo/platforms/sentinel2/processing_baseline.py +3 -0
- mapchete_eo/platforms/sentinel2/product.py +83 -18
- mapchete_eo/platforms/sentinel2/source.py +114 -0
- mapchete_eo/platforms/sentinel2/types.py +5 -0
- mapchete_eo/product.py +14 -8
- mapchete_eo/protocols.py +5 -0
- mapchete_eo/search/__init__.py +3 -3
- mapchete_eo/search/base.py +127 -99
- mapchete_eo/search/config.py +75 -4
- mapchete_eo/search/s2_mgrs.py +8 -9
- mapchete_eo/search/stac_search.py +99 -97
- mapchete_eo/search/stac_static.py +46 -102
- mapchete_eo/search/utm_search.py +54 -62
- mapchete_eo/settings.py +1 -0
- mapchete_eo/sort.py +4 -6
- mapchete_eo/source.py +107 -0
- {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/METADATA +4 -3
- mapchete_eo-2026.1.0.dist-info/RECORD +89 -0
- {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/WHEEL +1 -1
- {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/entry_points.txt +1 -1
- {mapchete_eo-2025.10.1.dist-info → mapchete_eo-2026.1.0.dist-info}/licenses/LICENSE +1 -1
- mapchete_eo/archives/__init__.py +0 -0
- mapchete_eo/archives/base.py +0 -65
- mapchete_eo/geometry.py +0 -271
- mapchete_eo/known_catalogs.py +0 -42
- mapchete_eo/platforms/sentinel2/archives.py +0 -190
- mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +0 -29
- mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +0 -34
- mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +0 -105
- mapchete_eo-2025.10.1.dist-info/RECORD +0 -88
mapchete_eo/protocols.py
CHANGED
|
@@ -15,6 +15,7 @@ from mapchete.io.raster import ReferencedRaster
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class EOProductProtocol(Protocol):
|
|
18
|
+
id: str
|
|
18
19
|
bounds: Bounds
|
|
19
20
|
crs: CRS
|
|
20
21
|
__geo_interface__: Optional[Dict[str, Any]]
|
|
@@ -54,3 +55,7 @@ class EOProductProtocol(Protocol):
|
|
|
54
55
|
|
|
55
56
|
class DateTimeProtocol(Protocol):
|
|
56
57
|
datetime: DateTimeLike
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GetPropertyProtocol(Protocol):
|
|
61
|
+
def get_property(self, property: str) -> Any: ...
|
mapchete_eo/search/__init__.py
CHANGED
|
@@ -7,8 +7,8 @@ of product metadata.
|
|
|
7
7
|
It helps the InputData class to find the input products and their metadata.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from mapchete_eo.search.stac_search import
|
|
11
|
-
from mapchete_eo.search.stac_static import
|
|
10
|
+
from mapchete_eo.search.stac_search import STACSearchCollection
|
|
11
|
+
from mapchete_eo.search.stac_static import STACStaticCollection
|
|
12
12
|
from mapchete_eo.search.utm_search import UTMSearchCatalog
|
|
13
13
|
|
|
14
|
-
__all__ = ["
|
|
14
|
+
__all__ = ["STACSearchCollection", "STACStaticCollection", "UTMSearchCatalog"]
|
mapchete_eo/search/base.py
CHANGED
|
@@ -2,20 +2,24 @@ from functools import cached_property
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
|
-
from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union
|
|
5
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, Set, Type, Union
|
|
6
6
|
|
|
7
|
+
from pygeofilter.parsers.ecql import parse as parse_ecql
|
|
8
|
+
from pygeofilter.backends.native.evaluate import NativeEvaluator
|
|
7
9
|
from pydantic import BaseModel
|
|
8
|
-
from pystac import Item, Catalog, CatalogType, Extent
|
|
9
10
|
from mapchete.path import MPath, MPathLike
|
|
10
11
|
from mapchete.types import Bounds
|
|
12
|
+
from pystac import Catalog, Item, CatalogType, Extent
|
|
11
13
|
from pystac.collection import Collection
|
|
12
14
|
from pystac.stac_io import DefaultStacIO
|
|
13
|
-
from pystac_client import
|
|
15
|
+
from pystac_client import CollectionClient
|
|
14
16
|
from pystac_client.stac_api_io import StacApiIO
|
|
15
17
|
from rasterio.profiles import Profile
|
|
16
18
|
from shapely.geometry.base import BaseGeometry
|
|
17
19
|
|
|
18
20
|
from mapchete_eo.io.assets import get_assets, get_metadata_assets
|
|
21
|
+
from mapchete_eo.product import blacklist_products
|
|
22
|
+
from mapchete_eo.settings import mapchete_eo_settings
|
|
19
23
|
from mapchete_eo.types import TimeRange
|
|
20
24
|
|
|
21
25
|
logger = logging.getLogger(__name__)
|
|
@@ -44,29 +48,51 @@ class FSSpecStacIO(StacApiIO):
|
|
|
44
48
|
return dst.write(json.dumps(json_dict, indent=2))
|
|
45
49
|
|
|
46
50
|
|
|
47
|
-
class
|
|
51
|
+
class CollectionSearcher(ABC):
|
|
48
52
|
"""
|
|
49
53
|
This class serves as a bridge between an Archive and a catalog implementation.
|
|
50
54
|
"""
|
|
51
55
|
|
|
52
|
-
collections: List[str]
|
|
53
56
|
config_cls: Type[BaseModel]
|
|
57
|
+
collection: str
|
|
58
|
+
stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None
|
|
59
|
+
blacklist: Set[str] = (
|
|
60
|
+
blacklist_products(mapchete_eo_settings.blacklist)
|
|
61
|
+
if mapchete_eo_settings.blacklist
|
|
62
|
+
else set()
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
collection: str,
|
|
68
|
+
stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None,
|
|
69
|
+
):
|
|
70
|
+
self.collection = collection
|
|
71
|
+
self.stac_item_modifiers = stac_item_modifiers
|
|
54
72
|
|
|
55
73
|
@abstractmethod
|
|
56
74
|
@cached_property
|
|
57
|
-
def
|
|
75
|
+
def client(self) -> CollectionClient: ...
|
|
58
76
|
|
|
59
77
|
@abstractmethod
|
|
60
78
|
@cached_property
|
|
61
|
-
def
|
|
79
|
+
def eo_bands(self) -> List[str]: ...
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def config(self) -> BaseModel:
|
|
83
|
+
return self.config_cls()
|
|
62
84
|
|
|
63
|
-
@abstractmethod
|
|
64
85
|
@cached_property
|
|
65
|
-
def
|
|
86
|
+
def id(self) -> str:
|
|
87
|
+
return self.client.id
|
|
88
|
+
|
|
89
|
+
@cached_property
|
|
90
|
+
def description(self) -> str:
|
|
91
|
+
return self.client.description
|
|
66
92
|
|
|
67
|
-
@abstractmethod
|
|
68
93
|
@cached_property
|
|
69
|
-
def stac_extensions(self) -> List[str]:
|
|
94
|
+
def stac_extensions(self) -> List[str]:
|
|
95
|
+
return self.client.stac_extensions
|
|
70
96
|
|
|
71
97
|
@abstractmethod
|
|
72
98
|
def search(
|
|
@@ -74,20 +100,17 @@ class CatalogSearcher(ABC):
|
|
|
74
100
|
time: Optional[Union[TimeRange, List[TimeRange]]] = None,
|
|
75
101
|
bounds: Optional[Bounds] = None,
|
|
76
102
|
area: Optional[BaseGeometry] = None,
|
|
103
|
+
query: Optional[str] = None,
|
|
77
104
|
search_kwargs: Optional[Dict[str, Any]] = None,
|
|
78
105
|
) -> Generator[Item, None, None]: ...
|
|
79
106
|
|
|
80
107
|
|
|
81
|
-
class
|
|
108
|
+
class StaticCollectionWriterMixin(CollectionSearcher):
|
|
82
109
|
# client: Client
|
|
83
110
|
# id: str
|
|
84
111
|
# description: str
|
|
85
112
|
# stac_extensions: List[str]
|
|
86
113
|
|
|
87
|
-
@abstractmethod
|
|
88
|
-
def get_collections(self) -> List[Collection]: # pragma: no cover
|
|
89
|
-
...
|
|
90
|
-
|
|
91
114
|
def write_static_catalog(
|
|
92
115
|
self,
|
|
93
116
|
output_path: MPathLike,
|
|
@@ -113,94 +136,93 @@ class StaticCatalogWriterMixin(CatalogSearcher):
|
|
|
113
136
|
catalog_json = output_path / "catalog.json"
|
|
114
137
|
if catalog_json.exists():
|
|
115
138
|
logger.debug("open existing catalog %s", str(catalog_json))
|
|
116
|
-
|
|
117
|
-
#
|
|
118
|
-
|
|
139
|
+
catalog = Catalog.from_file(catalog_json)
|
|
140
|
+
# client = Client.from_file(catalog_json)
|
|
141
|
+
# existing_collection = client.get_collection(self.id)
|
|
119
142
|
else:
|
|
120
|
-
existing_collections = []
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
# existing_collections = []
|
|
144
|
+
catalog = Catalog(
|
|
145
|
+
name or f"{self.id}",
|
|
146
|
+
description or f"Static subset of {self.description}",
|
|
147
|
+
stac_extensions=self.stac_extensions,
|
|
148
|
+
href=str(catalog_json),
|
|
149
|
+
catalog_type=CatalogType.SELF_CONTAINED,
|
|
150
|
+
)
|
|
128
151
|
src_items = list(
|
|
129
152
|
self.search(
|
|
130
153
|
time=time, bounds=bounds, area=area, search_kwargs=search_kwargs
|
|
131
154
|
)
|
|
132
155
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
item
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
item
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
break
|
|
156
|
+
# collect all items and download assets if required
|
|
157
|
+
items: List[Item] = []
|
|
158
|
+
item_ids = set()
|
|
159
|
+
for n, item in enumerate(src_items, 1):
|
|
160
|
+
logger.debug("found item %s", item)
|
|
161
|
+
item = item.clone()
|
|
162
|
+
if assets:
|
|
163
|
+
logger.debug("get assets %s", assets)
|
|
164
|
+
item = get_assets(
|
|
165
|
+
item,
|
|
166
|
+
assets,
|
|
167
|
+
output_path / self.id / item.id,
|
|
168
|
+
resolution=assets_dst_resolution,
|
|
169
|
+
convert_profile=assets_convert_profile,
|
|
170
|
+
overwrite=overwrite,
|
|
171
|
+
ignore_if_exists=True,
|
|
172
|
+
)
|
|
173
|
+
if copy_metadata:
|
|
174
|
+
item = get_metadata_assets(
|
|
175
|
+
item,
|
|
176
|
+
output_path / self.id / item.id,
|
|
177
|
+
metadata_parser_classes=metadata_parser_classes,
|
|
178
|
+
resolution=assets_dst_resolution,
|
|
179
|
+
convert_profile=assets_convert_profile,
|
|
180
|
+
overwrite=overwrite,
|
|
181
|
+
)
|
|
182
|
+
# this has to be set to None, otherwise pystac will mess up the asset paths
|
|
183
|
+
# after normalizing
|
|
184
|
+
item.set_self_href(None)
|
|
185
|
+
|
|
186
|
+
items.append(item)
|
|
187
|
+
item_ids.add(item.id)
|
|
188
|
+
|
|
189
|
+
if progress_callback:
|
|
190
|
+
progress_callback(n=n, total=len(src_items))
|
|
191
|
+
|
|
192
|
+
# for existing_collection in existing_collections:
|
|
193
|
+
# if existing_collection.id == collection.id:
|
|
194
|
+
# logger.debug("try to find unregistered items in collection")
|
|
195
|
+
# collection_root_path = MPath.from_inp(
|
|
196
|
+
# existing_collection.get_self_href()
|
|
197
|
+
# ).parent
|
|
198
|
+
# for subpath in collection_root_path.ls():
|
|
199
|
+
# if subpath.is_directory():
|
|
200
|
+
# try:
|
|
201
|
+
# item = Item.from_file(
|
|
202
|
+
# subpath / subpath.with_suffix(".json").name
|
|
203
|
+
# )
|
|
204
|
+
# if item.id not in item_ids:
|
|
205
|
+
# logger.debug(
|
|
206
|
+
# "add existing item with id %s", item.id
|
|
207
|
+
# )
|
|
208
|
+
# items.append(item)
|
|
209
|
+
# item_ids.add(item.id)
|
|
210
|
+
# except FileNotFoundError:
|
|
211
|
+
# pass
|
|
212
|
+
# break
|
|
191
213
|
# create collection and copy metadata
|
|
192
214
|
logger.debug("create new collection")
|
|
193
215
|
out_collection = Collection(
|
|
194
|
-
id=
|
|
216
|
+
id=self.id,
|
|
195
217
|
extent=Extent.from_items(items),
|
|
196
|
-
description=
|
|
197
|
-
title=
|
|
198
|
-
stac_extensions=
|
|
199
|
-
license=
|
|
200
|
-
keywords=
|
|
201
|
-
providers=
|
|
202
|
-
summaries=
|
|
203
|
-
extra_fields=
|
|
218
|
+
description=self.description,
|
|
219
|
+
title=self.client.title,
|
|
220
|
+
stac_extensions=self.stac_extensions,
|
|
221
|
+
license=self.client.license,
|
|
222
|
+
keywords=self.client.keywords,
|
|
223
|
+
providers=self.client.providers,
|
|
224
|
+
summaries=self.client.summaries,
|
|
225
|
+
extra_fields=self.client.extra_fields,
|
|
204
226
|
catalog_type=CatalogType.SELF_CONTAINED,
|
|
205
227
|
)
|
|
206
228
|
|
|
@@ -222,14 +244,20 @@ class StaticCatalogWriterMixin(CatalogSearcher):
|
|
|
222
244
|
|
|
223
245
|
def filter_items(
|
|
224
246
|
items: Generator[Item, None, None],
|
|
225
|
-
|
|
226
|
-
max_cloud_cover: float = 100.0,
|
|
247
|
+
query: Optional[str] = None,
|
|
227
248
|
) -> Generator[Item, None, None]:
|
|
228
249
|
"""
|
|
229
250
|
Only for cloudcover now, this can and should be adapted for filter field and value
|
|
230
251
|
the field and value for the item filter would be defined in search.config.py corresponding configs
|
|
231
252
|
and passed down to the individual search approaches via said config and this Function.
|
|
232
253
|
"""
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
if query:
|
|
255
|
+
ast = parse_ecql(query)
|
|
256
|
+
evaluator = NativeEvaluator(use_getattr=False)
|
|
257
|
+
filter_func = evaluator.evaluate(ast)
|
|
258
|
+
for item in items:
|
|
259
|
+
# pystac items store metadata in 'properties'
|
|
260
|
+
if filter_func(item.properties):
|
|
261
|
+
yield item
|
|
262
|
+
else:
|
|
263
|
+
yield from items
|
mapchete_eo/search/config.py
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
2
6
|
|
|
3
7
|
from mapchete.path import MPath, MPathLike
|
|
4
|
-
from pydantic import BaseModel
|
|
8
|
+
from pydantic import BaseModel, model_validator
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
class StacSearchConfig(BaseModel):
|
|
8
12
|
max_cloud_cover: float = 100.0
|
|
13
|
+
query: Optional[str] = None
|
|
9
14
|
catalog_chunk_threshold: int = 10_000
|
|
10
15
|
catalog_chunk_zoom: int = 5
|
|
11
16
|
catalog_pagesize: int = 100
|
|
12
17
|
footprint_buffer: float = 0
|
|
13
18
|
|
|
19
|
+
@model_validator(mode="before")
|
|
20
|
+
def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
21
|
+
if "max_cloud_cover" in values: # pragma: no cover
|
|
22
|
+
raise DeprecationWarning(
|
|
23
|
+
"'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
|
|
24
|
+
)
|
|
25
|
+
return values
|
|
26
|
+
|
|
14
27
|
|
|
15
28
|
class StacStaticConfig(BaseModel):
|
|
16
|
-
|
|
29
|
+
@model_validator(mode="before")
|
|
30
|
+
def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
if "max_cloud_cover" in values: # pragma: no cover
|
|
32
|
+
raise DeprecationWarning(
|
|
33
|
+
"'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
|
|
34
|
+
)
|
|
35
|
+
return values
|
|
17
36
|
|
|
18
37
|
|
|
19
38
|
class UTMSearchConfig(BaseModel):
|
|
20
|
-
|
|
39
|
+
@model_validator(mode="before")
|
|
40
|
+
def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
41
|
+
if "max_cloud_cover" in values: # pragma: no cover
|
|
42
|
+
raise DeprecationWarning(
|
|
43
|
+
"'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
|
|
44
|
+
)
|
|
45
|
+
return values
|
|
21
46
|
|
|
22
47
|
sinergise_aws_collections: dict = dict(
|
|
23
48
|
S2_L2A=dict(
|
|
@@ -25,18 +50,64 @@ class UTMSearchConfig(BaseModel):
|
|
|
25
50
|
path=MPath(
|
|
26
51
|
"https://sentinel-s2-l2a-stac.s3.amazonaws.com/sentinel-s2-l2a.json"
|
|
27
52
|
),
|
|
53
|
+
endpoint="s3://sentinel-s2-l2a-stac",
|
|
28
54
|
),
|
|
29
55
|
S2_L1C=dict(
|
|
30
56
|
id="sentinel-s2-l1c",
|
|
31
57
|
path=MPath(
|
|
32
58
|
"https://sentinel-s2-l1c-stac.s3.amazonaws.com/sentinel-s2-l1c.json"
|
|
33
59
|
),
|
|
60
|
+
endpoint="s3://sentinel-s2-l1c-stac",
|
|
34
61
|
),
|
|
35
62
|
S1_GRD=dict(
|
|
36
63
|
id="sentinel-s1-l1c",
|
|
37
64
|
path=MPath(
|
|
38
65
|
"https://sentinel-s1-l1c-stac.s3.amazonaws.com/sentinel-s1-l1c.json"
|
|
39
66
|
),
|
|
67
|
+
endpoint="s3://sentinel-s1-l1c-stac",
|
|
40
68
|
),
|
|
41
69
|
)
|
|
42
70
|
search_index: Optional[MPathLike] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@contextmanager
|
|
74
|
+
def patch_invalid_assets():
|
|
75
|
+
"""
|
|
76
|
+
Context manager/decorator to fix pystac crash on malformed assets (strings instead of dicts).
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
from pystac.extensions.file import FileExtensionHooks
|
|
81
|
+
except ImportError: # pragma: no cover
|
|
82
|
+
yield
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
logger = logging.getLogger(__name__)
|
|
86
|
+
|
|
87
|
+
_original_migrate = FileExtensionHooks.migrate
|
|
88
|
+
|
|
89
|
+
def _safe_migrate(self, obj, version, info):
|
|
90
|
+
if "assets" in obj and isinstance(obj["assets"], dict):
|
|
91
|
+
bad_keys = []
|
|
92
|
+
for key, asset in obj["assets"].items():
|
|
93
|
+
if not isinstance(asset, dict):
|
|
94
|
+
logger.debug(
|
|
95
|
+
"Removing malformed asset '%s' (type %s) from item %s",
|
|
96
|
+
key,
|
|
97
|
+
type(asset),
|
|
98
|
+
obj.get("id", "unknown"),
|
|
99
|
+
)
|
|
100
|
+
bad_keys.append(key)
|
|
101
|
+
|
|
102
|
+
for key in bad_keys:
|
|
103
|
+
del obj["assets"][key]
|
|
104
|
+
|
|
105
|
+
return _original_migrate(self, obj, version, info)
|
|
106
|
+
|
|
107
|
+
# Apply patch
|
|
108
|
+
FileExtensionHooks.migrate = _safe_migrate
|
|
109
|
+
try:
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
# Restore original
|
|
113
|
+
FileExtensionHooks.migrate = _original_migrate
|
mapchete_eo/search/s2_mgrs.py
CHANGED
|
@@ -6,18 +6,17 @@ from functools import cached_property
|
|
|
6
6
|
from itertools import product
|
|
7
7
|
from typing import List, Literal, Optional, Tuple, Union
|
|
8
8
|
|
|
9
|
-
from mapchete.geometry import
|
|
9
|
+
from mapchete.geometry import (
|
|
10
|
+
reproject_geometry,
|
|
11
|
+
repair_antimeridian_geometry,
|
|
12
|
+
transform_to_latlon,
|
|
13
|
+
)
|
|
10
14
|
from mapchete.types import Bounds
|
|
11
15
|
from rasterio.crs import CRS
|
|
12
16
|
from shapely import prepare
|
|
13
17
|
from shapely.geometry import box, mapping, shape
|
|
14
18
|
from shapely.geometry.base import BaseGeometry
|
|
15
19
|
|
|
16
|
-
from mapchete_eo.geometry import (
|
|
17
|
-
bounds_to_geom,
|
|
18
|
-
repair_antimeridian_geometry,
|
|
19
|
-
transform_to_latlon,
|
|
20
|
-
)
|
|
21
20
|
|
|
22
21
|
LATLON_LEFT = -180
|
|
23
22
|
LATLON_RIGHT = 180
|
|
@@ -255,7 +254,7 @@ class S2Tile:
|
|
|
255
254
|
grid_square = tile_id[3:]
|
|
256
255
|
try:
|
|
257
256
|
int(utm_zone)
|
|
258
|
-
except Exception:
|
|
257
|
+
except Exception: # pragma: no cover
|
|
259
258
|
raise ValueError(f"invalid UTM zone given: {utm_zone}")
|
|
260
259
|
|
|
261
260
|
return MGRSCell(utm_zone, latitude_band).tile(grid_square)
|
|
@@ -268,7 +267,7 @@ class S2Tile:
|
|
|
268
267
|
def s2_tiles_from_bounds(
|
|
269
268
|
left: float, bottom: float, right: float, top: float
|
|
270
269
|
) -> List[S2Tile]:
|
|
271
|
-
bounds = Bounds(left, bottom, right, top)
|
|
270
|
+
bounds = Bounds(left, bottom, right, top, crs="EPSG:4326")
|
|
272
271
|
|
|
273
272
|
# determine zones in eastern-western direction
|
|
274
273
|
min_zone_idx = math.floor((left + LATLON_WIDTH_OFFSET) / UTM_ZONE_WIDTH)
|
|
@@ -291,7 +290,7 @@ def s2_tiles_from_bounds(
|
|
|
291
290
|
min_latitude_band_idx -= 1
|
|
292
291
|
max_latitude_band_idx += 1
|
|
293
292
|
|
|
294
|
-
aoi =
|
|
293
|
+
aoi = bounds.latlon_geometry()
|
|
295
294
|
prepare(aoi)
|
|
296
295
|
|
|
297
296
|
def tiles_generator():
|