mapchete-eo 2025.7.0__tar.gz

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-2025.7.0/.gitignore +13 -0
  2. mapchete_eo-2025.7.0/LICENSE +21 -0
  3. mapchete_eo-2025.7.0/PKG-INFO +38 -0
  4. mapchete_eo-2025.7.0/README.md +2 -0
  5. mapchete_eo-2025.7.0/mapchete_eo/__init__.py +1 -0
  6. mapchete_eo-2025.7.0/mapchete_eo/archives/__init__.py +0 -0
  7. mapchete_eo-2025.7.0/mapchete_eo/archives/base.py +65 -0
  8. mapchete_eo-2025.7.0/mapchete_eo/array/__init__.py +0 -0
  9. mapchete_eo-2025.7.0/mapchete_eo/array/buffer.py +16 -0
  10. mapchete_eo-2025.7.0/mapchete_eo/array/color.py +29 -0
  11. mapchete_eo-2025.7.0/mapchete_eo/array/convert.py +157 -0
  12. mapchete_eo-2025.7.0/mapchete_eo/base.py +528 -0
  13. mapchete_eo-2025.7.0/mapchete_eo/blacklist.txt +175 -0
  14. mapchete_eo-2025.7.0/mapchete_eo/cli/__init__.py +30 -0
  15. mapchete_eo-2025.7.0/mapchete_eo/cli/bounds.py +22 -0
  16. mapchete_eo-2025.7.0/mapchete_eo/cli/options_arguments.py +243 -0
  17. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_brdf.py +77 -0
  18. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_cat_results.py +146 -0
  19. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_find_broken_products.py +93 -0
  20. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  21. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_mask.py +71 -0
  22. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_mgrs.py +45 -0
  23. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_rgb.py +114 -0
  24. mapchete_eo-2025.7.0/mapchete_eo/cli/s2_verify.py +129 -0
  25. mapchete_eo-2025.7.0/mapchete_eo/cli/static_catalog.py +123 -0
  26. mapchete_eo-2025.7.0/mapchete_eo/eostac.py +30 -0
  27. mapchete_eo-2025.7.0/mapchete_eo/exceptions.py +87 -0
  28. mapchete_eo-2025.7.0/mapchete_eo/geometry.py +271 -0
  29. mapchete_eo-2025.7.0/mapchete_eo/image_operations/__init__.py +12 -0
  30. mapchete_eo-2025.7.0/mapchete_eo/image_operations/color_correction.py +136 -0
  31. mapchete_eo-2025.7.0/mapchete_eo/image_operations/compositing.py +247 -0
  32. mapchete_eo-2025.7.0/mapchete_eo/image_operations/dtype_scale.py +43 -0
  33. mapchete_eo-2025.7.0/mapchete_eo/image_operations/fillnodata.py +130 -0
  34. mapchete_eo-2025.7.0/mapchete_eo/image_operations/filters.py +319 -0
  35. mapchete_eo-2025.7.0/mapchete_eo/image_operations/linear_normalization.py +81 -0
  36. mapchete_eo-2025.7.0/mapchete_eo/image_operations/sigmoidal.py +114 -0
  37. mapchete_eo-2025.7.0/mapchete_eo/io/__init__.py +37 -0
  38. mapchete_eo-2025.7.0/mapchete_eo/io/assets.py +492 -0
  39. mapchete_eo-2025.7.0/mapchete_eo/io/items.py +147 -0
  40. mapchete_eo-2025.7.0/mapchete_eo/io/levelled_cubes.py +228 -0
  41. mapchete_eo-2025.7.0/mapchete_eo/io/path.py +144 -0
  42. mapchete_eo-2025.7.0/mapchete_eo/io/products.py +413 -0
  43. mapchete_eo-2025.7.0/mapchete_eo/io/profiles.py +45 -0
  44. mapchete_eo-2025.7.0/mapchete_eo/known_catalogs.py +42 -0
  45. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  46. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/archives.py +190 -0
  47. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  48. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  49. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  50. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  51. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  52. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  53. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  54. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  55. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  56. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/config.py +181 -0
  57. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/driver.py +78 -0
  58. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/masks.py +325 -0
  59. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
  60. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
  61. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
  62. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
  63. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
  64. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
  65. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
  66. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
  67. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/product.py +669 -0
  68. mapchete_eo-2025.7.0/mapchete_eo/platforms/sentinel2/types.py +109 -0
  69. mapchete_eo-2025.7.0/mapchete_eo/processes/__init__.py +0 -0
  70. mapchete_eo-2025.7.0/mapchete_eo/processes/config.py +51 -0
  71. mapchete_eo-2025.7.0/mapchete_eo/processes/dtype_scale.py +112 -0
  72. mapchete_eo-2025.7.0/mapchete_eo/processes/eo_to_xarray.py +19 -0
  73. mapchete_eo-2025.7.0/mapchete_eo/processes/merge_rasters.py +235 -0
  74. mapchete_eo-2025.7.0/mapchete_eo/product.py +278 -0
  75. mapchete_eo-2025.7.0/mapchete_eo/protocols.py +56 -0
  76. mapchete_eo-2025.7.0/mapchete_eo/search/__init__.py +14 -0
  77. mapchete_eo-2025.7.0/mapchete_eo/search/base.py +222 -0
  78. mapchete_eo-2025.7.0/mapchete_eo/search/config.py +42 -0
  79. mapchete_eo-2025.7.0/mapchete_eo/search/s2_mgrs.py +314 -0
  80. mapchete_eo-2025.7.0/mapchete_eo/search/stac_search.py +251 -0
  81. mapchete_eo-2025.7.0/mapchete_eo/search/stac_static.py +236 -0
  82. mapchete_eo-2025.7.0/mapchete_eo/search/utm_search.py +251 -0
  83. mapchete_eo-2025.7.0/mapchete_eo/settings.py +24 -0
  84. mapchete_eo-2025.7.0/mapchete_eo/sort.py +48 -0
  85. mapchete_eo-2025.7.0/mapchete_eo/time.py +53 -0
  86. mapchete_eo-2025.7.0/mapchete_eo/types.py +73 -0
  87. mapchete_eo-2025.7.0/pyproject.toml +74 -0
@@ -0,0 +1,13 @@
1
+ .eggs
2
+ *.egg-info
3
+ *.pyc
4
+ .cache
5
+ htmlcov
6
+ .coverage
7
+ .coverage.*
8
+ build/
9
+ dist/
10
+ .pytest*
11
+ *.gfs
12
+ .vscode/
13
+ __pycache__
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 - 2025 EOX IT Services
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: mapchete-eo
3
+ Version: 2025.7.0
4
+ Summary: mapchete EO data reader
5
+ Project-URL: Homepage, https://gitlab.eox.at/maps/mapchete_eo
6
+ Author-email: Joachim Ungar <joachim.ungar@eox.at>, Petr Sevcik <petr.sevcik@eox.at>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: GIS
16
+ Requires-Dist: blend-modes
17
+ Requires-Dist: click
18
+ Requires-Dist: croniter
19
+ Requires-Dist: lxml
20
+ Requires-Dist: mapchete[complete]>=2025.6.0
21
+ Requires-Dist: opencv-python
22
+ Requires-Dist: pillow
23
+ Requires-Dist: pydantic
24
+ Requires-Dist: pystac-client>=0.7.5
25
+ Requires-Dist: pystac[urllib3]>=1.12.2
26
+ Requires-Dist: retry
27
+ Requires-Dist: rtree
28
+ Requires-Dist: scipy
29
+ Requires-Dist: tqdm
30
+ Requires-Dist: xarray
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest-coverage; extra == 'test'
33
+ Requires-Dist: pytest-lazy-fixture; extra == 'test'
34
+ Requires-Dist: pytest<8; extra == 'test'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # Mapchete EO driver
38
+
@@ -0,0 +1,2 @@
1
+ # Mapchete EO driver
2
+
@@ -0,0 +1 @@
1
+ __version__ = "2025.7.0"
File without changes
@@ -0,0 +1,65 @@
1
+ from abc import ABC
2
+ import logging
3
+ from typing import Any, Callable, Dict, Generator, List, Optional, Union
4
+
5
+ from mapchete.io.vector import IndexedFeatures
6
+ from mapchete.types import Bounds
7
+ from pystac import Item
8
+ from shapely.errors import GEOSException
9
+ from shapely.geometry.base import BaseGeometry
10
+
11
+ from mapchete_eo.exceptions import ItemGeometryError
12
+ from mapchete_eo.search.base import CatalogSearcher
13
+ from mapchete_eo.types import TimeRange
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Archive(ABC):
19
+ """
20
+ An archive combines a Catalog and a Storage.
21
+ """
22
+
23
+ time: Union[TimeRange, List[TimeRange]]
24
+ area: BaseGeometry
25
+ catalog: CatalogSearcher
26
+ search_kwargs: Dict[str, Any]
27
+ _items: Optional[IndexedFeatures] = None
28
+ item_modifier_funcs: Optional[List[Callable[[Item], Item]]] = None
29
+
30
+ def __init__(
31
+ self,
32
+ time: Union[TimeRange, List[TimeRange]],
33
+ bounds: Optional[Bounds] = None,
34
+ area: Optional[BaseGeometry] = None,
35
+ search_kwargs: Optional[Dict[str, Any]] = None,
36
+ catalog: Optional[CatalogSearcher] = None,
37
+ ):
38
+ if bounds is None and area is None:
39
+ raise ValueError("either bounds or area have to be provided")
40
+ elif area is None:
41
+ area = Bounds.from_inp(bounds).geometry
42
+ self.time = time
43
+ self.area = area
44
+ self.search_kwargs = search_kwargs or {}
45
+ if catalog:
46
+ self.catalog = catalog
47
+
48
+ def get_catalog_config(self):
49
+ return self.catalog.config_cls(**self.search_kwargs)
50
+
51
+ def apply_item_modifier_funcs(self, item: Item) -> Item:
52
+ try:
53
+ for modifier in self.item_modifier_funcs or []:
54
+ item = modifier(item)
55
+ except GEOSException as exc:
56
+ raise ItemGeometryError(
57
+ f"item {item.get_self_href()} geometry could not be resolved: {str(exc)}"
58
+ )
59
+ return item
60
+
61
+ def items(self) -> Generator[Item, None, None]:
62
+ for item in self.catalog.search(
63
+ time=self.time, area=self.area, search_kwargs=self.search_kwargs
64
+ ):
65
+ yield self.apply_item_modifier_funcs(item)
File without changes
@@ -0,0 +1,16 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ from numpy.typing import DTypeLike
5
+ from scipy.ndimage import binary_dilation
6
+
7
+
8
+ def buffer_array(
9
+ array: np.ndarray, buffer: int = 0, out_array_dtype: Optional[DTypeLike] = None
10
+ ) -> np.ndarray:
11
+ if out_array_dtype is None:
12
+ out_array_dtype = array.dtype
13
+ if buffer == 0:
14
+ return array.astype(out_array_dtype, copy=False)
15
+
16
+ return binary_dilation(array, iterations=buffer).astype(out_array_dtype, copy=False)
@@ -0,0 +1,29 @@
1
+ import numpy as np
2
+ import numpy.ma as ma
3
+
4
+
5
+ def color_array(shape: tuple, hex_color: str):
6
+ colors = hex_to_rgb(hex_color)
7
+ return ma.masked_array(
8
+ [np.full(shape, color, dtype=np.uint8) for color in colors],
9
+ mask=ma.zeros((len(colors), *shape)),
10
+ )
11
+
12
+
13
+ def hex_to_rgb(hex_color):
14
+ """
15
+ Convert hex color to tuple of RGB(A) colors.
16
+
17
+ e.g. "#FFFFFF" --> (255, 255, 255) or "#00FF00FF" --> (0, 255, 0, 255)
18
+ """
19
+ channels = iter(hex_color.lstrip("#"))
20
+ return tuple(int("".join(channel), 16) for channel in zip(channels, channels))
21
+
22
+
23
+ def outlier_pixels(
24
+ arr: np.ndarray,
25
+ axis: int = 0,
26
+ range_threshold: int = 100,
27
+ ) -> np.ndarray:
28
+ """Detect outlier pixels containing extreme colors."""
29
+ return arr.max(axis=axis) - arr.min(axis=axis) >= range_threshold
@@ -0,0 +1,157 @@
1
+ from typing import List, Optional, Union
2
+
3
+ import numpy as np
4
+ import numpy.ma as ma
5
+ import xarray as xr
6
+ from mapchete.types import NodataVal
7
+
8
+ # dtypes from https://numpy.org/doc/stable/user/basics.types.html
9
+ _NUMPY_FLOAT_DTYPES = [
10
+ np.half,
11
+ np.float16,
12
+ np.single,
13
+ np.double,
14
+ np.longdouble,
15
+ np.csingle,
16
+ np.cdouble,
17
+ np.clongdouble,
18
+ ]
19
+
20
+
21
+ def to_masked_array(
22
+ xarr: Union[xr.Dataset, xr.DataArray], copy: bool = False
23
+ ) -> ma.MaskedArray:
24
+ """Convert xr.DataArray to ma.MaskedArray."""
25
+ if isinstance(xarr, xr.Dataset):
26
+ xarr = xarr.to_array()
27
+
28
+ fill_value = xarr.attrs.get("_FillValue")
29
+ if fill_value is None:
30
+ raise ValueError(
31
+ "Cannot create masked_array because DataArray fill value is None"
32
+ )
33
+
34
+ if xarr.dtype in _NUMPY_FLOAT_DTYPES:
35
+ return ma.masked_values(xarr, fill_value, copy=copy, shrink=False)
36
+ else:
37
+ out = ma.masked_equal(xarr, fill_value, copy=copy)
38
+ # in case of a shrinked mask we have to expand it to the full array shape
39
+ if not isinstance(out.mask, np.ndarray):
40
+ out.mask = np.full(out.mask.shape, out.mask, dtype=bool)
41
+ return out
42
+
43
+
44
+ def to_dataarray(
45
+ masked_arr: ma.MaskedArray,
46
+ nodataval: NodataVal = None,
47
+ name: Optional[str] = None,
48
+ band_names: Optional[List[str]] = None,
49
+ band_axis_name: str = "bands",
50
+ x_axis_name: str = "x",
51
+ y_axis_name: str = "y",
52
+ attrs: Optional[dict] = None,
53
+ ) -> xr.DataArray:
54
+ """
55
+ Convert ma.MaskedArray to xr.DataArray.
56
+
57
+ Depending on whether the array is 2D or 3D, the axes will be named accordingly.
58
+
59
+ A 2-dimensional array indicates that we only have a spatial x- and y-axis. A
60
+ 3rd dimension will be interpreted as bands.
61
+ """
62
+ # nodata handling is weird.
63
+ #
64
+ # xr.DataArray cannot hold a masked_array but will turn it into
65
+ # a usual NumPy array, replacing the masked values with np.nan.
66
+ # However, this also seems to change the dtype to float32 which
67
+ # is not desirable.
68
+ nodataval = masked_arr.fill_value if nodataval is None else nodataval
69
+ attrs = attrs or dict()
70
+
71
+ if masked_arr.ndim == 2:
72
+ dims = [x_axis_name, y_axis_name]
73
+ coords = None
74
+ elif masked_arr.ndim == 3:
75
+ bands_count = masked_arr.shape[0]
76
+ band_names = band_names or [f"{band_axis_name}-{i}" for i in range(bands_count)]
77
+ dims = [band_axis_name, x_axis_name, y_axis_name]
78
+ coords = {band_axis_name: band_names}
79
+ else: # pragma: no cover
80
+ raise TypeError("only a 2D or 3D ma.MaskedArray is allowed.")
81
+
82
+ return xr.DataArray(
83
+ data=masked_arr.filled(nodataval),
84
+ dims=dims,
85
+ name=name,
86
+ attrs=dict(attrs, _FillValue=nodataval),
87
+ coords=coords,
88
+ )
89
+
90
+
91
+ def to_dataset(
92
+ masked_arr: ma.MaskedArray,
93
+ nodataval: NodataVal = None,
94
+ slice_names: Optional[List[str]] = None,
95
+ band_names: Optional[List[str]] = None,
96
+ slices_attrs: Optional[List[Union[dict, None]]] = None,
97
+ slice_axis_name: str = "time",
98
+ band_axis_name: str = "bands",
99
+ x_axis_name: str = "x",
100
+ y_axis_name: str = "y",
101
+ attrs: Optional[dict] = None,
102
+ ):
103
+ """Convert a 3D or 4D ma.MaskedArray to an xarray.Dataset."""
104
+ attrs = attrs or dict()
105
+ nodataval = masked_arr.fill_value if nodataval is None else nodataval
106
+
107
+ if masked_arr.ndim == 3:
108
+ bands = masked_arr.shape[0]
109
+ band_names = band_names or [f"{band_axis_name}-{i}" for i in range(bands)]
110
+ raise NotImplementedError()
111
+ elif masked_arr.ndim == 4:
112
+ slices, bands = masked_arr.shape[:2]
113
+ band_names = band_names or [f"{band_axis_name}-{i}" for i in range(bands)]
114
+ slice_names = slice_names or [f"{slice_axis_name}-{i}" for i in range(slices)]
115
+ slices_attrs = (
116
+ [None for _ in range(slices)] if slices_attrs is None else slices_attrs
117
+ )
118
+ coords = {slice_axis_name: slice_names}
119
+ return xr.Dataset(
120
+ data_vars={
121
+ # every slice gets its own xarray Dataset
122
+ slice_name: to_dataarray(
123
+ slice_array,
124
+ nodataval=nodataval,
125
+ band_names=band_names,
126
+ name=slice_name,
127
+ attrs=slice_attrs,
128
+ band_axis_name=band_axis_name,
129
+ x_axis_name=x_axis_name,
130
+ y_axis_name=y_axis_name,
131
+ )
132
+ for slice_name, slice_attrs, slice_array in zip(
133
+ slice_names,
134
+ slices_attrs,
135
+ masked_arr,
136
+ )
137
+ },
138
+ coords=coords,
139
+ attrs=dict(attrs, _FillValue=nodataval),
140
+ ).transpose(slice_axis_name, band_axis_name, x_axis_name, y_axis_name)
141
+
142
+ else: # pragma: no cover
143
+ raise TypeError("only a 3D or 4D ma.MaskedArray is allowed.")
144
+
145
+
146
+ def to_bands_mask(arr: np.ndarray, bands: int = 1) -> np.ndarray:
147
+ """Expands a 2D mask to a full band mask."""
148
+ if arr.ndim != 2:
149
+ raise TypeError("input array has to have exactly 2 dimensions.")
150
+ return np.repeat(
151
+ np.expand_dims(
152
+ arr,
153
+ axis=0,
154
+ ),
155
+ bands,
156
+ axis=0,
157
+ )