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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/archives/__init__.py +0 -0
  3. mapchete_eo/archives/base.py +65 -0
  4. mapchete_eo/array/__init__.py +0 -0
  5. mapchete_eo/array/buffer.py +16 -0
  6. mapchete_eo/array/color.py +29 -0
  7. mapchete_eo/array/convert.py +157 -0
  8. mapchete_eo/base.py +528 -0
  9. mapchete_eo/blacklist.txt +175 -0
  10. mapchete_eo/cli/__init__.py +30 -0
  11. mapchete_eo/cli/bounds.py +22 -0
  12. mapchete_eo/cli/options_arguments.py +243 -0
  13. mapchete_eo/cli/s2_brdf.py +77 -0
  14. mapchete_eo/cli/s2_cat_results.py +146 -0
  15. mapchete_eo/cli/s2_find_broken_products.py +93 -0
  16. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  17. mapchete_eo/cli/s2_mask.py +71 -0
  18. mapchete_eo/cli/s2_mgrs.py +45 -0
  19. mapchete_eo/cli/s2_rgb.py +114 -0
  20. mapchete_eo/cli/s2_verify.py +129 -0
  21. mapchete_eo/cli/static_catalog.py +123 -0
  22. mapchete_eo/eostac.py +30 -0
  23. mapchete_eo/exceptions.py +87 -0
  24. mapchete_eo/geometry.py +271 -0
  25. mapchete_eo/image_operations/__init__.py +12 -0
  26. mapchete_eo/image_operations/color_correction.py +136 -0
  27. mapchete_eo/image_operations/compositing.py +247 -0
  28. mapchete_eo/image_operations/dtype_scale.py +43 -0
  29. mapchete_eo/image_operations/fillnodata.py +130 -0
  30. mapchete_eo/image_operations/filters.py +319 -0
  31. mapchete_eo/image_operations/linear_normalization.py +81 -0
  32. mapchete_eo/image_operations/sigmoidal.py +114 -0
  33. mapchete_eo/io/__init__.py +37 -0
  34. mapchete_eo/io/assets.py +492 -0
  35. mapchete_eo/io/items.py +147 -0
  36. mapchete_eo/io/levelled_cubes.py +228 -0
  37. mapchete_eo/io/path.py +144 -0
  38. mapchete_eo/io/products.py +413 -0
  39. mapchete_eo/io/profiles.py +45 -0
  40. mapchete_eo/known_catalogs.py +42 -0
  41. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  42. mapchete_eo/platforms/sentinel2/archives.py +190 -0
  43. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  44. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  45. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  46. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  47. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  48. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  49. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  50. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  51. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  52. mapchete_eo/platforms/sentinel2/config.py +181 -0
  53. mapchete_eo/platforms/sentinel2/driver.py +78 -0
  54. mapchete_eo/platforms/sentinel2/masks.py +325 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
  56. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
  57. mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
  58. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
  59. mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
  60. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
  63. mapchete_eo/platforms/sentinel2/product.py +669 -0
  64. mapchete_eo/platforms/sentinel2/types.py +109 -0
  65. mapchete_eo/processes/__init__.py +0 -0
  66. mapchete_eo/processes/config.py +51 -0
  67. mapchete_eo/processes/dtype_scale.py +112 -0
  68. mapchete_eo/processes/eo_to_xarray.py +19 -0
  69. mapchete_eo/processes/merge_rasters.py +235 -0
  70. mapchete_eo/product.py +278 -0
  71. mapchete_eo/protocols.py +56 -0
  72. mapchete_eo/search/__init__.py +14 -0
  73. mapchete_eo/search/base.py +222 -0
  74. mapchete_eo/search/config.py +42 -0
  75. mapchete_eo/search/s2_mgrs.py +314 -0
  76. mapchete_eo/search/stac_search.py +251 -0
  77. mapchete_eo/search/stac_static.py +236 -0
  78. mapchete_eo/search/utm_search.py +251 -0
  79. mapchete_eo/settings.py +24 -0
  80. mapchete_eo/sort.py +48 -0
  81. mapchete_eo/time.py +53 -0
  82. mapchete_eo/types.py +73 -0
  83. mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
  84. mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
  85. mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
  86. mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
  87. mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,492 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import math
5
+ from typing import Callable, List, Optional, Union
6
+
7
+ import fsspec
8
+ import numpy as np
9
+ import numpy.ma as ma
10
+ import pystac
11
+ from affine import Affine
12
+ from mapchete import Timer
13
+ from mapchete.io import copy, fiona_open, rasterio_open
14
+ from mapchete.io.raster import ReferencedRaster, read_raster, read_raster_window
15
+ from mapchete.path import MPath
16
+ from mapchete.protocols import GridProtocol
17
+ from mapchete.settings import IORetrySettings
18
+ from mapchete.types import Grid, NodataVal
19
+ from numpy.typing import DTypeLike
20
+ from pydantic import BaseModel
21
+ from rasterio.dtypes import dtype_ranges
22
+ from rasterio.enums import Resampling
23
+ from rasterio.features import rasterize
24
+ from rasterio.profiles import Profile
25
+ from rasterio.vrt import WarpedVRT
26
+ from retry import retry
27
+
28
+ from mapchete_eo.io.path import COMMON_RASTER_EXTENSIONS, asset_mpath, cached_path
29
+ from mapchete_eo.io.profiles import COGDeflateProfile
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class STACRasterBandProperties(BaseModel):
35
+ nodata: NodataVal = None
36
+ data_type: Optional[str] = None
37
+ scale: float = 1.0
38
+ offset: float = 0.0
39
+
40
+ @staticmethod
41
+ def from_asset(
42
+ asset: pystac.Asset,
43
+ nodataval: NodataVal = None,
44
+ ) -> STACRasterBandProperties:
45
+ if asset.extra_fields.get("raster:offset") is not None:
46
+ properties = dict(
47
+ offset=asset.extra_fields.get("raster:offset"),
48
+ scale=asset.extra_fields.get("raster:scale"),
49
+ nodata=asset.extra_fields.get("nodata", nodataval),
50
+ )
51
+ else:
52
+ properties = asset.extra_fields.get("raster:bands", [{}])[0]
53
+ properties.update(
54
+ nodata=(
55
+ nodataval
56
+ if properties.get("nodata") is None
57
+ else properties.get("nodata")
58
+ ),
59
+ )
60
+
61
+ return STACRasterBandProperties(
62
+ **properties,
63
+ )
64
+
65
+
66
+ def asset_to_np_array(
67
+ item: pystac.Item,
68
+ asset: str,
69
+ indexes: Union[List[int], int] = 1,
70
+ grid: Optional[GridProtocol] = None,
71
+ resampling: Resampling = Resampling.nearest,
72
+ nodataval: NodataVal = None,
73
+ apply_offset: bool = True,
74
+ ) -> ma.MaskedArray:
75
+ """
76
+ Read grid window of STAC Items and merge into a 2D ma.MaskedArray.
77
+
78
+ This is the main read method which is one way or the other being called from everywhere
79
+ whenever a band is being read!
80
+ """
81
+ # get path early to catch an eventual asset missing error early
82
+ path = asset_mpath(item, asset)
83
+
84
+ # find out asset details if raster:bands is available
85
+ stac_raster_bands = STACRasterBandProperties.from_asset(
86
+ item.assets[asset], nodataval=nodataval
87
+ )
88
+
89
+ logger.debug("reading asset %s and indexes %s ...", asset, indexes)
90
+ data = read_raster(
91
+ inp=path,
92
+ indexes=indexes,
93
+ grid=grid,
94
+ resampling=resampling.name,
95
+ dst_nodata=stac_raster_bands.nodata,
96
+ ).data
97
+
98
+ if apply_offset and stac_raster_bands.offset:
99
+ data_type = stac_raster_bands.data_type or data.dtype
100
+
101
+ # determine value range for the target data_type
102
+ clip_min, clip_max = dtype_ranges[str(data_type)]
103
+
104
+ # increase minimum clip value to avoid collission with nodata value
105
+ if clip_min == stac_raster_bands.nodata:
106
+ clip_min += 1
107
+
108
+ data[:] = (
109
+ (
110
+ ((data * stac_raster_bands.scale) + stac_raster_bands.offset)
111
+ / stac_raster_bands.scale
112
+ )
113
+ .round()
114
+ .clip(clip_min, clip_max)
115
+ .astype(data_type, copy=False)
116
+ .data
117
+ )
118
+
119
+ return data
120
+
121
+
122
+ def get_assets(
123
+ item: pystac.Item,
124
+ assets: List[str],
125
+ dst_dir: MPath,
126
+ src_fs: fsspec.AbstractFileSystem = None,
127
+ overwrite: bool = False,
128
+ resolution: Union[None, float, int] = None,
129
+ convert_profile: Optional[Profile] = None,
130
+ item_href_in_dst_dir: bool = True,
131
+ ignore_if_exists: bool = False,
132
+ ) -> pystac.Item:
133
+ """
134
+ Copy or convert assets depending on settings.
135
+
136
+ Conversion is triggered if either resolution or convert_profile is provided.
137
+ """
138
+ for asset in assets:
139
+ path = asset_mpath(item, asset, fs=src_fs)
140
+ # convert if possible
141
+ if should_be_converted(path, resolution=resolution, profile=convert_profile):
142
+ item = convert_asset(
143
+ item,
144
+ asset,
145
+ dst_dir,
146
+ src_fs=src_fs,
147
+ resolution=resolution,
148
+ overwrite=overwrite,
149
+ ignore_if_exists=ignore_if_exists,
150
+ profile=convert_profile or COGDeflateProfile(),
151
+ item_href_in_dst_dir=item_href_in_dst_dir,
152
+ )
153
+ continue
154
+
155
+ # copy
156
+ item = copy_asset(
157
+ item,
158
+ asset,
159
+ dst_dir,
160
+ overwrite=overwrite,
161
+ ignore_if_exists=ignore_if_exists,
162
+ item_href_in_dst_dir=item_href_in_dst_dir,
163
+ )
164
+
165
+ return item
166
+
167
+
168
+ def copy_asset(
169
+ item: pystac.Item,
170
+ asset: str,
171
+ dst_dir: MPath,
172
+ src_fs: fsspec.AbstractFileSystem = None,
173
+ overwrite: bool = False,
174
+ item_href_in_dst_dir: bool = True,
175
+ ignore_if_exists: bool = False,
176
+ ) -> pystac.Item:
177
+ """Copy asset from one place to another."""
178
+ src_path = asset_mpath(item, asset, fs=src_fs)
179
+ output_path = dst_dir / src_path.name
180
+
181
+ # write relative path into asset.href if Item will be in the same directory
182
+ if item_href_in_dst_dir and not output_path.is_absolute(): # pragma: no cover
183
+ item.assets[asset].href = src_path.name
184
+ else:
185
+ item.assets[asset].href = str(output_path)
186
+
187
+ # TODO make this check optional
188
+ if output_path.exists():
189
+ if ignore_if_exists:
190
+ logger.debug("ignore existing asset %s", output_path)
191
+ return item
192
+ elif overwrite:
193
+ logger.debug("overwrite exsiting asset %s", output_path)
194
+ else:
195
+ raise IOError(f"{output_path} already exists")
196
+ else:
197
+ dst_dir.makedirs()
198
+
199
+ with Timer() as t:
200
+ logger.debug("copy asset %s to %s ...", src_path, dst_dir)
201
+ copy(
202
+ src_path,
203
+ output_path,
204
+ overwrite=overwrite,
205
+ )
206
+ logger.debug("copied asset '%s' in %s", asset, t)
207
+
208
+ return item
209
+
210
+
211
+ def convert_asset(
212
+ item: pystac.Item,
213
+ asset: str,
214
+ dst_dir: MPath,
215
+ src_fs: fsspec.AbstractFileSystem = None,
216
+ overwrite: bool = False,
217
+ resolution: Union[None, float, int] = None,
218
+ profile: Optional[Profile] = None,
219
+ item_href_in_dst_dir: bool = True,
220
+ ignore_if_exists: bool = False,
221
+ ) -> pystac.Item:
222
+ """
223
+ Convert asset to a different format.
224
+ """
225
+ src_path = asset_mpath(item, asset, fs=src_fs)
226
+ output_path = dst_dir / src_path.name
227
+ profile = profile or COGDeflateProfile()
228
+
229
+ # write relative path into asset.href if Item will be in the same directory
230
+ if item_href_in_dst_dir and not output_path.is_absolute(): # pragma: no cover
231
+ item.assets[asset].href = src_path.name
232
+ else:
233
+ item.assets[asset].href = str(output_path)
234
+
235
+ # TODO make this check optional
236
+ if output_path.exists():
237
+ if ignore_if_exists:
238
+ logger.debug("ignore existing asset %s", output_path)
239
+ return item
240
+ elif overwrite:
241
+ logger.debug("overwrite exsiting asset %s", output_path)
242
+ else:
243
+ raise IOError(f"{output_path} already exists")
244
+ else:
245
+ dst_dir.makedirs()
246
+
247
+ with Timer() as t:
248
+ convert_raster(src_path, output_path, resolution, profile)
249
+ logger.debug("converted asset '%s' in %s", asset, t)
250
+
251
+ return item
252
+
253
+
254
+ @retry(logger=logger, **dict(IORetrySettings()))
255
+ def convert_raster(
256
+ src_path: MPath,
257
+ dst_path: MPath,
258
+ resolution: Union[None, float, int] = None,
259
+ profile: Optional[Profile] = None,
260
+ ) -> None:
261
+ with rasterio_open(src_path, "r") as src:
262
+ meta = src.meta.copy()
263
+ if profile:
264
+ meta.update(**profile)
265
+ src_transform = src.transform
266
+ if resolution:
267
+ logger.debug(
268
+ "converting %s to %s using %sm resolution with profile %s ...",
269
+ src_path,
270
+ dst_path,
271
+ resolution,
272
+ profile,
273
+ )
274
+ src_res = src.transform[0]
275
+ dst_transform = Affine.from_gdal(
276
+ *(
277
+ src_transform[2],
278
+ resolution,
279
+ 0.0,
280
+ src_transform[5],
281
+ 0.0,
282
+ -resolution,
283
+ )
284
+ )
285
+ dst_width = int(math.ceil(src.width * (src_res / resolution)))
286
+ dst_height = int(math.ceil(src.height * (src_res / resolution)))
287
+ meta.update(
288
+ transform=dst_transform,
289
+ width=dst_width,
290
+ height=dst_height,
291
+ )
292
+ logger.debug("convert %s to %s with settings %s", src_path, dst_path, meta)
293
+ with rasterio_open(dst_path, "w", **meta) as dst:
294
+ with WarpedVRT(
295
+ src,
296
+ width=meta["width"],
297
+ height=meta["height"],
298
+ transform=meta["transform"],
299
+ ) as warped:
300
+ dst.write(warped.read())
301
+
302
+
303
+ def get_metadata_assets(
304
+ item: pystac.Item,
305
+ dst_dir: MPath,
306
+ overwrite: bool = False,
307
+ metadata_parser_classes: Optional[tuple] = None,
308
+ resolution: Union[None, float, int] = None,
309
+ convert_profile: Optional[Profile] = None,
310
+ metadata_asset_names: List[str] = ["metadata", "granule_metadata"],
311
+ ):
312
+ """Copy STAC item metadata and its metadata assets."""
313
+ for metadata_asset in metadata_asset_names:
314
+ try:
315
+ src_metadata_xml = MPath(item.assets[metadata_asset].href)
316
+ break
317
+ except KeyError:
318
+ pass
319
+ else: # pragma: no cover
320
+ raise KeyError("no 'metadata' or 'granule_metadata' asset found")
321
+
322
+ # copy metadata.xml
323
+ dst_metadata_xml = dst_dir / src_metadata_xml.name
324
+ if overwrite or not dst_metadata_xml.exists():
325
+ copy(src_metadata_xml, dst_metadata_xml, overwrite=overwrite)
326
+
327
+ item.assets[metadata_asset].href = src_metadata_xml.name
328
+ if metadata_parser_classes is None: # pragma: no cover
329
+ raise TypeError("no metadata parser class given")
330
+
331
+ for metadata_parser_cls in metadata_parser_classes:
332
+ src_metadata = metadata_parser_cls.from_metadata_xml(src_metadata_xml)
333
+ dst_metadata = metadata_parser_cls.from_metadata_xml(dst_metadata_xml)
334
+ break
335
+ else: # pragma: no cover
336
+ raise TypeError(
337
+ f"could not parse {src_metadata_xml} with {metadata_parser_classes}"
338
+ )
339
+
340
+ # copy assets
341
+ original_asset_paths = src_metadata.assets
342
+ for asset, dst_path in dst_metadata.assets.items():
343
+ src_path = original_asset_paths[asset]
344
+
345
+ if overwrite or not dst_path.exists():
346
+ # convert if possible
347
+ if should_be_converted(
348
+ src_path, resolution=resolution, profile=convert_profile
349
+ ): # pragma: no cover
350
+ convert_raster(src_path, dst_path, resolution, convert_profile)
351
+ else:
352
+ logger.debug("copy %s ...", asset)
353
+ copy(src_path, dst_path, overwrite=overwrite)
354
+
355
+ return item
356
+
357
+
358
+ @retry(logger=logger, **dict(IORetrySettings()))
359
+ def should_be_converted(
360
+ path: MPath,
361
+ resolution: Union[None, float, int] = None,
362
+ profile: Optional[Profile] = None,
363
+ ) -> bool:
364
+ """Decide whether a raster file should be converted or not."""
365
+ if path.endswith(tuple(COMMON_RASTER_EXTENSIONS)):
366
+ # see if it even pays off to convert based on resolution
367
+ if resolution is not None:
368
+ with rasterio_open(path) as src:
369
+ src_resolution = src.transform[0]
370
+ if src_resolution != resolution:
371
+ return True
372
+
373
+ # when profile is given, check if profile differs from remote file
374
+ elif profile is not None:
375
+ with rasterio_open(path) as src:
376
+ for key, value in profile.items():
377
+ if value == "COG":
378
+ # TODO check if file is really a valid cog
379
+ value = "GTiff"
380
+ if key in src.meta and src.meta[key] != value:
381
+ logger.debug(
382
+ "different value for %s required: %s should become %s",
383
+ key,
384
+ src.meta[key],
385
+ value,
386
+ )
387
+ return True
388
+ return False
389
+
390
+ return False
391
+
392
+
393
+ def _read_vector_mask(mask_path):
394
+ logger.debug("open %s with Fiona", mask_path)
395
+ with cached_path(mask_path) as cached:
396
+ try:
397
+ with fiona_open(cached) as src:
398
+ return list([dict(f) for f in src])
399
+ except ValueError as e:
400
+ # this happens if GML file is empty
401
+ for message in ["Null layer: ", "'hLayer' is NULL in 'OGR_L_GetName'"]:
402
+ if message in str(e):
403
+ return []
404
+ else: # pragma: no cover
405
+ raise
406
+
407
+
408
+ @retry(logger=logger, **dict(IORetrySettings()))
409
+ def read_mask_as_raster(
410
+ path: MPath,
411
+ indexes: Optional[List[int]] = None,
412
+ dst_grid: Optional[GridProtocol] = None,
413
+ resampling: Resampling = Resampling.nearest,
414
+ rasterize_value_func: Callable = lambda feature: feature.get("id", 1),
415
+ rasterize_feature_filter: Callable = lambda feature: True,
416
+ dtype: Optional[DTypeLike] = None,
417
+ masked: bool = True,
418
+ cached_read: bool = False,
419
+ ) -> ReferencedRaster:
420
+ """
421
+ Read mask as array regardless of source data type (raster or vector).
422
+ """
423
+ if dst_grid:
424
+ dst_grid = Grid.from_obj(dst_grid)
425
+ try:
426
+ with cached_path(path, active=cached_read) as path:
427
+ if path.suffix in COMMON_RASTER_EXTENSIONS:
428
+ if dst_grid:
429
+ array = read_raster_window(
430
+ path, grid=dst_grid, indexes=indexes, resampling=resampling
431
+ )
432
+ # sum up bands to 2D mask and keep dtype
433
+ array = array.sum(axis=0, dtype=array.dtype)
434
+ mask = ReferencedRaster(
435
+ data=array if masked else array.data,
436
+ transform=dst_grid.transform,
437
+ bounds=dst_grid.bounds,
438
+ crs=dst_grid.crs,
439
+ )
440
+ else:
441
+ with rasterio_open(path) as src:
442
+ mask = ReferencedRaster(
443
+ src.read(indexes, masked=masked).sum(
444
+ axis=0, dtype=src.dtypes[0]
445
+ ),
446
+ transform=src.transform,
447
+ bounds=src.bounds,
448
+ crs=src.crs,
449
+ )
450
+
451
+ # make sure output has correct dtype
452
+ if dtype:
453
+ mask.data = mask.data.astype(dtype)
454
+ return mask
455
+
456
+ else:
457
+ if dst_grid:
458
+ features = [
459
+ feature
460
+ for feature in _read_vector_mask(path)
461
+ if rasterize_feature_filter(feature)
462
+ ]
463
+ features_values = [
464
+ (feature["geometry"], rasterize_value_func(feature))
465
+ for feature in features
466
+ ]
467
+ return ReferencedRaster(
468
+ data=(
469
+ rasterize(
470
+ features_values,
471
+ out_shape=dst_grid.shape,
472
+ transform=dst_grid.transform,
473
+ dtype=np.uint8,
474
+ ).astype(dtype)
475
+ if features_values
476
+ else np.zeros(dst_grid.shape, dtype=dtype)
477
+ ),
478
+ transform=dst_grid.transform,
479
+ crs=dst_grid.crs,
480
+ bounds=dst_grid.bounds,
481
+ )
482
+ else: # pragma: no cover
483
+ raise ValueError("out_shape and out_transform have to be provided.")
484
+ except Exception as exception: # pragma: no cover
485
+ # This is a hack because some tool using aiohttp does not raise a
486
+ # ClientResponseError directly but masks it as a generic Exception and thus
487
+ # preventing our retry mechanism to kick in.
488
+ if repr(exception).startswith('Exception("ClientResponseError'):
489
+ raise ConnectionError(repr(exception)).with_traceback(
490
+ exception.__traceback__
491
+ ) from exception
492
+ raise
@@ -0,0 +1,147 @@
1
+ import logging
2
+ from typing import Any, List, Optional
3
+
4
+ import numpy.ma as ma
5
+ import pystac
6
+ from mapchete.protocols import GridProtocol
7
+ from mapchete.types import Bounds, NodataVals
8
+ from rasterio.enums import Resampling
9
+ from shapely.geometry import mapping, shape
10
+
11
+ from mapchete_eo.exceptions import EmptyProductException
12
+ from mapchete_eo.geometry import repair_antimeridian_geometry
13
+ from mapchete_eo.io.assets import asset_to_np_array
14
+ from mapchete_eo.types import BandLocation
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def item_to_np_array(
20
+ item: pystac.Item,
21
+ band_locations: List[BandLocation],
22
+ grid: Optional[GridProtocol] = None,
23
+ resampling: Resampling = Resampling.nearest,
24
+ nodatavals: NodataVals = None,
25
+ raise_empty: bool = False,
26
+ apply_offset: bool = True,
27
+ ) -> ma.MaskedArray:
28
+ """
29
+ Read window of STAC Item and merge into a 3D ma.MaskedArray.
30
+ """
31
+ logger.debug("reading %s assets from item %s...", len(band_locations), item.id)
32
+ out = ma.stack(
33
+ [
34
+ asset_to_np_array(
35
+ item,
36
+ band_location.asset_name,
37
+ indexes=band_location.band_index,
38
+ grid=grid,
39
+ resampling=expanded_resampling,
40
+ nodataval=nodataval,
41
+ apply_offset=apply_offset,
42
+ )
43
+ for band_location, expanded_resampling, nodataval in zip(
44
+ band_locations,
45
+ expand_params(resampling, len(band_locations)),
46
+ expand_params(nodatavals, len(band_locations)),
47
+ )
48
+ ]
49
+ )
50
+
51
+ if raise_empty and out.mask.all():
52
+ raise EmptyProductException(
53
+ f"all required assets of {item} over grid {grid} are empty."
54
+ )
55
+
56
+ return out
57
+
58
+
59
+ def expand_params(param, length):
60
+ """
61
+ Expand parameters if they are not a list.
62
+ """
63
+ if isinstance(param, list):
64
+ if len(param) != length:
65
+ raise ValueError(f"length of {param} must be {length} but is {len(param)}")
66
+ return param
67
+ return [param for _ in range(length)]
68
+
69
+
70
+ def get_item_property(
71
+ item: pystac.Item,
72
+ property: str,
73
+ ) -> Any:
74
+ """
75
+ Return item property.
76
+
77
+ A valid property can be a special property like "year" from the items datetime property
78
+ or any key in the item properties or extra_fields.
79
+
80
+ Search order of properties is based on the pystac LayoutTemplate search order:
81
+
82
+ https://pystac.readthedocs.io/en/stable/_modules/pystac/layout.html#LayoutTemplate
83
+ - The object's attributes
84
+ - Keys in the ``properties`` attribute, if it exists.
85
+ - Keys in the ``extra_fields`` attribute, if it exists.
86
+
87
+ Some special keys can be used in template variables:
88
+
89
+ +--------------------+--------------------------------------------------------+
90
+ | Template variable | Meaning |
91
+ +====================+========================================================+
92
+ | ``year`` | The year of an Item's datetime, or |
93
+ | | start_datetime if datetime is null |
94
+ +--------------------+--------------------------------------------------------+
95
+ | ``month`` | The month of an Item's datetime, or |
96
+ | | start_datetime if datetime is null |
97
+ +--------------------+--------------------------------------------------------+
98
+ | ``day`` | The day of an Item's datetime, or |
99
+ | | start_datetime if datetime is null |
100
+ +--------------------+--------------------------------------------------------+
101
+ | ``date`` | The date (iso format) of an Item's |
102
+ | | datetime, or start_datetime if datetime is null |
103
+ +--------------------+--------------------------------------------------------+
104
+ | ``collection`` | The collection ID of an Item's collection. |
105
+ +--------------------+--------------------------------------------------------+
106
+ """
107
+ if property in ["year", "month", "day", "date", "datetime"]:
108
+ if item.datetime is None:
109
+ raise ValueError(
110
+ f"STAC item has no datetime attached, thus cannot get property {property}"
111
+ )
112
+ elif property == "date":
113
+ return item.datetime.date().isoformat()
114
+ elif property == "datetime":
115
+ return item.datetime
116
+ else:
117
+ return item.datetime.__getattribute__(property)
118
+ elif property == "collection":
119
+ return item.collection_id
120
+ elif property in item.properties:
121
+ return item.properties[property]
122
+ elif property in item.extra_fields:
123
+ return item.extra_fields[property]
124
+ elif property == "stac_extensions":
125
+ return item.stac_extensions
126
+ else:
127
+ raise KeyError(
128
+ f"item {item.id} does not have property {property} in its datetime, properties "
129
+ f"({', '.join(item.properties.keys())}) or extra_fields "
130
+ f"({', '.join(item.extra_fields.keys())})"
131
+ )
132
+
133
+
134
+ def item_fix_footprint(
135
+ item: pystac.Item, bbox_width_threshold: float = 180.0
136
+ ) -> pystac.Item:
137
+ bounds = Bounds.from_inp(item.bbox)
138
+
139
+ if bounds.width > bbox_width_threshold:
140
+ logger.debug("item %s crosses Antimeridian, fixing ...", item.id)
141
+
142
+ if item.geometry:
143
+ geometry = repair_antimeridian_geometry(geometry=shape(item.geometry))
144
+ item.geometry = mapping(geometry)
145
+ item.bbox = list(geometry.bounds)
146
+
147
+ return item