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.
- mapchete_eo/__init__.py +1 -0
- mapchete_eo/archives/__init__.py +0 -0
- mapchete_eo/archives/base.py +65 -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 +157 -0
- mapchete_eo/base.py +528 -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 +243 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +146 -0
- mapchete_eo/cli/s2_find_broken_products.py +93 -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 +123 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/geometry.py +271 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +247 -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 +492 -0
- mapchete_eo/io/items.py +147 -0
- mapchete_eo/io/levelled_cubes.py +228 -0
- mapchete_eo/io/path.py +144 -0
- mapchete_eo/io/products.py +413 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/known_catalogs.py +42 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/archives.py +190 -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 +181 -0
- mapchete_eo/platforms/sentinel2/driver.py +78 -0
- mapchete_eo/platforms/sentinel2/masks.py +325 -0
- mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
- mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
- mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
- mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
- mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
- mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
- mapchete_eo/platforms/sentinel2/product.py +669 -0
- mapchete_eo/platforms/sentinel2/types.py +109 -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 +235 -0
- mapchete_eo/product.py +278 -0
- mapchete_eo/protocols.py +56 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +222 -0
- mapchete_eo/search/config.py +42 -0
- mapchete_eo/search/s2_mgrs.py +314 -0
- mapchete_eo/search/stac_search.py +251 -0
- mapchete_eo/search/stac_static.py +236 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +24 -0
- mapchete_eo/sort.py +48 -0
- mapchete_eo/time.py +53 -0
- mapchete_eo/types.py +73 -0
- mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
- mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
- mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
- mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
- mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
mapchete_eo/io/assets.py
ADDED
|
@@ -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
|
mapchete_eo/io/items.py
ADDED
|
@@ -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
|