mapchete-eo 2025.10.0__py2.py3-none-any.whl → 2025.11.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 (66) hide show
  1. mapchete_eo/__init__.py +1 -1
  2. mapchete_eo/array/convert.py +7 -1
  3. mapchete_eo/base.py +123 -55
  4. mapchete_eo/cli/options_arguments.py +11 -27
  5. mapchete_eo/cli/s2_brdf.py +1 -1
  6. mapchete_eo/cli/s2_cat_results.py +4 -20
  7. mapchete_eo/cli/s2_find_broken_products.py +4 -20
  8. mapchete_eo/cli/s2_jp2_static_catalog.py +2 -2
  9. mapchete_eo/cli/static_catalog.py +4 -45
  10. mapchete_eo/eostac.py +1 -1
  11. mapchete_eo/io/assets.py +7 -7
  12. mapchete_eo/io/items.py +37 -22
  13. mapchete_eo/io/levelled_cubes.py +66 -35
  14. mapchete_eo/io/path.py +19 -8
  15. mapchete_eo/io/products.py +37 -27
  16. mapchete_eo/platforms/sentinel2/__init__.py +1 -1
  17. mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
  18. mapchete_eo/platforms/sentinel2/brdf/correction.py +1 -1
  19. mapchete_eo/platforms/sentinel2/brdf/hls.py +1 -1
  20. mapchete_eo/platforms/sentinel2/brdf/models.py +1 -1
  21. mapchete_eo/platforms/sentinel2/brdf/protocols.py +1 -1
  22. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +1 -1
  23. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +1 -1
  24. mapchete_eo/platforms/sentinel2/config.py +73 -13
  25. mapchete_eo/platforms/sentinel2/driver.py +0 -39
  26. mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
  27. mapchete_eo/platforms/sentinel2/{path_mappers → metadata_parser}/base.py +1 -1
  28. mapchete_eo/platforms/sentinel2/{path_mappers/metadata_xml.py → metadata_parser/default_path_mapper.py} +2 -2
  29. mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
  30. mapchete_eo/platforms/sentinel2/{metadata_parser.py → metadata_parser/s2metadata.py} +51 -146
  31. mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
  32. mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
  33. mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
  34. mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
  35. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +22 -1
  36. mapchete_eo/platforms/sentinel2/processing_baseline.py +3 -0
  37. mapchete_eo/platforms/sentinel2/product.py +88 -23
  38. mapchete_eo/platforms/sentinel2/source.py +114 -0
  39. mapchete_eo/platforms/sentinel2/types.py +5 -0
  40. mapchete_eo/processes/merge_rasters.py +7 -3
  41. mapchete_eo/product.py +14 -9
  42. mapchete_eo/protocols.py +5 -0
  43. mapchete_eo/search/__init__.py +3 -3
  44. mapchete_eo/search/base.py +126 -100
  45. mapchete_eo/search/config.py +25 -4
  46. mapchete_eo/search/s2_mgrs.py +8 -9
  47. mapchete_eo/search/stac_search.py +111 -75
  48. mapchete_eo/search/stac_static.py +63 -94
  49. mapchete_eo/search/utm_search.py +39 -48
  50. mapchete_eo/settings.py +1 -0
  51. mapchete_eo/sort.py +16 -2
  52. mapchete_eo/source.py +107 -0
  53. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/METADATA +2 -1
  54. mapchete_eo-2025.11.0.dist-info/RECORD +89 -0
  55. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/entry_points.txt +1 -1
  56. mapchete_eo/archives/__init__.py +0 -0
  57. mapchete_eo/archives/base.py +0 -65
  58. mapchete_eo/geometry.py +0 -271
  59. mapchete_eo/known_catalogs.py +0 -42
  60. mapchete_eo/platforms/sentinel2/archives.py +0 -190
  61. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +0 -29
  62. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +0 -34
  63. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +0 -105
  64. mapchete_eo-2025.10.0.dist-info/RECORD +0 -88
  65. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/WHEEL +0 -0
  66. {mapchete_eo-2025.10.0.dist-info → mapchete_eo-2025.11.0.dist-info}/licenses/LICENSE +0 -0
mapchete_eo/product.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any, List, Literal, Optional, Set
5
5
 
6
6
  import numpy as np
7
7
  import numpy.ma as ma
8
- import pystac
8
+ from pystac import Item
9
9
  import xarray as xr
10
10
  from mapchete import Timer
11
11
  from mapchete.io.raster import ReferencedRaster
@@ -26,15 +26,19 @@ logger = logging.getLogger(__name__)
26
26
 
27
27
 
28
28
  class EOProduct(EOProductProtocol):
29
- """Wrapper class around a pystac.Item which provides read functions."""
29
+ """Wrapper class around a Item which provides read functions."""
30
30
 
31
+ id: str
31
32
  default_dtype: DTypeLike = np.uint16
33
+ _item: Optional[Item] = None
32
34
 
33
- def __init__(self, item: pystac.Item):
35
+ def __init__(self, item: Item):
34
36
  self.item_dict = item.to_dict()
35
37
  self.__geo_interface__ = self.item.geometry
36
38
  self.bounds = Bounds.from_inp(shape(self))
37
39
  self.crs = mapchete_eo_settings.default_catalog_crs
40
+ self._item = None
41
+ self.id = item.id
38
42
 
39
43
  def __repr__(self):
40
44
  return f"<EOProduct product_id={self.item.id}>"
@@ -43,11 +47,13 @@ class EOProduct(EOProductProtocol):
43
47
  pass
44
48
 
45
49
  @property
46
- def item(self) -> pystac.Item:
47
- return pystac.Item.from_dict(self.item_dict)
50
+ def item(self) -> Item:
51
+ if not self._item:
52
+ self._item = Item.from_dict(self.item_dict)
53
+ return self._item
48
54
 
49
55
  @classmethod
50
- def from_stac_item(self, item: pystac.Item, **kwargs) -> EOProduct:
56
+ def from_stac_item(self, item: Item, **kwargs) -> EOProduct:
51
57
  return EOProduct(item)
52
58
 
53
59
  def get_mask(self) -> ReferencedRaster: ...
@@ -113,7 +119,6 @@ class EOProduct(EOProductProtocol):
113
119
  nodatavals: NodataVals = None,
114
120
  raise_empty: bool = True,
115
121
  apply_offset: bool = True,
116
- apply_scale: bool = False,
117
122
  **kwargs,
118
123
  ) -> ma.MaskedArray:
119
124
  assets = assets or []
@@ -172,7 +177,7 @@ class EOProduct(EOProductProtocol):
172
177
 
173
178
 
174
179
  def eo_bands_to_band_locations(
175
- item: pystac.Item,
180
+ item: Item,
176
181
  eo_bands: List[str],
177
182
  role: Literal["data", "reflectance", "visual"] = "data",
178
183
  ) -> List[BandLocation]:
@@ -183,7 +188,7 @@ def eo_bands_to_band_locations(
183
188
 
184
189
 
185
190
  def find_eo_band(
186
- item: pystac.Item,
191
+ item: Item,
187
192
  eo_band_name: str,
188
193
  role: Literal["data", "reflectance", "visual"] = "data",
189
194
  ) -> BandLocation:
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: ...
@@ -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 STACSearchCatalog
11
- from mapchete_eo.search.stac_static import STACStaticCatalog
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__ = ["STACSearchCatalog", "STACStaticCatalog", "UTMSearchCatalog"]
14
+ __all__ = ["STACSearchCollection", "STACStaticCollection", "UTMSearchCatalog"]
@@ -1,15 +1,17 @@
1
+ from functools import cached_property
1
2
  import json
2
3
  import logging
3
4
  from abc import ABC, abstractmethod
4
5
  from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union
5
6
 
7
+ from cql2 import Expr
6
8
  from pydantic import BaseModel
7
- from pystac import Item, Catalog, CatalogType, Extent
8
9
  from mapchete.path import MPath, MPathLike
9
10
  from mapchete.types import Bounds
11
+ from pystac import Catalog, Item, CatalogType, Extent
10
12
  from pystac.collection import Collection
11
13
  from pystac.stac_io import DefaultStacIO
12
- from pystac_client import Client
14
+ from pystac_client import CollectionClient
13
15
  from pystac_client.stac_api_io import StacApiIO
14
16
  from rasterio.profiles import Profile
15
17
  from shapely.geometry.base import BaseGeometry
@@ -43,17 +45,42 @@ class FSSpecStacIO(StacApiIO):
43
45
  return dst.write(json.dumps(json_dict, indent=2))
44
46
 
45
47
 
46
- class CatalogSearcher(ABC):
48
+ class CollectionSearcher(ABC):
47
49
  """
48
50
  This class serves as a bridge between an Archive and a catalog implementation.
49
51
  """
50
52
 
51
- eo_bands: List[str]
52
- id: str
53
- description: str
54
- stac_extensions: List[str]
55
- collections: List[str]
56
53
  config_cls: Type[BaseModel]
54
+ collection: str
55
+ stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None
56
+
57
+ def __init__(
58
+ self,
59
+ collection: str,
60
+ stac_item_modifiers: Optional[List[Callable[[Item], Item]]] = None,
61
+ ):
62
+ self.collection = collection
63
+ self.stac_item_modifiers = stac_item_modifiers
64
+
65
+ @abstractmethod
66
+ @cached_property
67
+ def client(self) -> CollectionClient: ...
68
+
69
+ @abstractmethod
70
+ @cached_property
71
+ def eo_bands(self) -> List[str]: ...
72
+
73
+ @abstractmethod
74
+ @cached_property
75
+ def id(self) -> str: ...
76
+
77
+ @abstractmethod
78
+ @cached_property
79
+ def description(self) -> str: ...
80
+
81
+ @abstractmethod
82
+ @cached_property
83
+ def stac_extensions(self) -> List[str]: ...
57
84
 
58
85
  @abstractmethod
59
86
  def search(
@@ -61,19 +88,16 @@ class CatalogSearcher(ABC):
61
88
  time: Optional[Union[TimeRange, List[TimeRange]]] = None,
62
89
  bounds: Optional[Bounds] = None,
63
90
  area: Optional[BaseGeometry] = None,
91
+ query: Optional[str] = None,
64
92
  search_kwargs: Optional[Dict[str, Any]] = None,
65
93
  ) -> Generator[Item, None, None]: ...
66
94
 
67
95
 
68
- class StaticCatalogWriterMixin(CatalogSearcher):
69
- client: Client
70
- id: str
71
- description: str
72
- stac_extensions: List[str]
73
-
74
- @abstractmethod
75
- def get_collections(self) -> List[Collection]: # pragma: no cover
76
- ...
96
+ class StaticCollectionWriterMixin(CollectionSearcher):
97
+ # client: Client
98
+ # id: str
99
+ # description: str
100
+ # stac_extensions: List[str]
77
101
 
78
102
  def write_static_catalog(
79
103
  self,
@@ -100,94 +124,93 @@ class StaticCatalogWriterMixin(CatalogSearcher):
100
124
  catalog_json = output_path / "catalog.json"
101
125
  if catalog_json.exists():
102
126
  logger.debug("open existing catalog %s", str(catalog_json))
103
- client = Client.from_file(catalog_json)
104
- # catalog = pystac.Catalog.from_file(catalog_json)
105
- existing_collections = list(client.get_collections())
127
+ catalog = Catalog.from_file(catalog_json)
128
+ # client = Client.from_file(catalog_json)
129
+ # existing_collection = client.get_collection(self.id)
106
130
  else:
107
- existing_collections = []
108
- catalog = Catalog(
109
- name or f"{self.id}",
110
- description or f"Static subset of {self.description}",
111
- stac_extensions=self.stac_extensions,
112
- href=str(catalog_json),
113
- catalog_type=CatalogType.SELF_CONTAINED,
114
- )
131
+ # existing_collections = []
132
+ catalog = Catalog(
133
+ name or f"{self.id}",
134
+ description or f"Static subset of {self.description}",
135
+ stac_extensions=self.stac_extensions,
136
+ href=str(catalog_json),
137
+ catalog_type=CatalogType.SELF_CONTAINED,
138
+ )
115
139
  src_items = list(
116
140
  self.search(
117
141
  time=time, bounds=bounds, area=area, search_kwargs=search_kwargs
118
142
  )
119
143
  )
120
- for collection in self.get_collections():
121
- # collect all items and download assets if required
122
- items: List[Item] = []
123
- item_ids = set()
124
- for n, item in enumerate(src_items, 1):
125
- logger.debug("found item %s", item)
126
- item = item.clone()
127
- if assets:
128
- logger.debug("get assets %s", assets)
129
- item = get_assets(
130
- item,
131
- assets,
132
- output_path / collection.id / item.id,
133
- resolution=assets_dst_resolution,
134
- convert_profile=assets_convert_profile,
135
- overwrite=overwrite,
136
- ignore_if_exists=True,
137
- )
138
- if copy_metadata:
139
- item = get_metadata_assets(
140
- item,
141
- output_path / collection.id / item.id,
142
- metadata_parser_classes=metadata_parser_classes,
143
- resolution=assets_dst_resolution,
144
- convert_profile=assets_convert_profile,
145
- overwrite=overwrite,
146
- )
147
- # this has to be set to None, otherwise pystac will mess up the asset paths
148
- # after normalizing
149
- item.set_self_href(None)
150
-
151
- items.append(item)
152
- item_ids.add(item.id)
153
-
154
- if progress_callback:
155
- progress_callback(n=n, total=len(src_items))
156
-
157
- for existing_collection in existing_collections:
158
- if existing_collection.id == collection.id:
159
- logger.debug("try to find unregistered items in collection")
160
- collection_root_path = MPath.from_inp(
161
- existing_collection.get_self_href()
162
- ).parent
163
- for subpath in collection_root_path.ls():
164
- if subpath.is_directory():
165
- try:
166
- item = Item.from_file(
167
- subpath / subpath.with_suffix(".json").name
168
- )
169
- if item.id not in item_ids:
170
- logger.debug(
171
- "add existing item with id %s", item.id
172
- )
173
- items.append(item)
174
- item_ids.add(item.id)
175
- except FileNotFoundError:
176
- pass
177
- break
144
+ # collect all items and download assets if required
145
+ items: List[Item] = []
146
+ item_ids = set()
147
+ for n, item in enumerate(src_items, 1):
148
+ logger.debug("found item %s", item)
149
+ item = item.clone()
150
+ if assets:
151
+ logger.debug("get assets %s", assets)
152
+ item = get_assets(
153
+ item,
154
+ assets,
155
+ output_path / self.id / item.id,
156
+ resolution=assets_dst_resolution,
157
+ convert_profile=assets_convert_profile,
158
+ overwrite=overwrite,
159
+ ignore_if_exists=True,
160
+ )
161
+ if copy_metadata:
162
+ item = get_metadata_assets(
163
+ item,
164
+ output_path / self.id / item.id,
165
+ metadata_parser_classes=metadata_parser_classes,
166
+ resolution=assets_dst_resolution,
167
+ convert_profile=assets_convert_profile,
168
+ overwrite=overwrite,
169
+ )
170
+ # this has to be set to None, otherwise pystac will mess up the asset paths
171
+ # after normalizing
172
+ item.set_self_href(None)
173
+
174
+ items.append(item)
175
+ item_ids.add(item.id)
176
+
177
+ if progress_callback:
178
+ progress_callback(n=n, total=len(src_items))
179
+
180
+ # for existing_collection in existing_collections:
181
+ # if existing_collection.id == collection.id:
182
+ # logger.debug("try to find unregistered items in collection")
183
+ # collection_root_path = MPath.from_inp(
184
+ # existing_collection.get_self_href()
185
+ # ).parent
186
+ # for subpath in collection_root_path.ls():
187
+ # if subpath.is_directory():
188
+ # try:
189
+ # item = Item.from_file(
190
+ # subpath / subpath.with_suffix(".json").name
191
+ # )
192
+ # if item.id not in item_ids:
193
+ # logger.debug(
194
+ # "add existing item with id %s", item.id
195
+ # )
196
+ # items.append(item)
197
+ # item_ids.add(item.id)
198
+ # except FileNotFoundError:
199
+ # pass
200
+ # break
178
201
  # create collection and copy metadata
179
202
  logger.debug("create new collection")
180
203
  out_collection = Collection(
181
- id=collection.id,
204
+ id=self.id,
182
205
  extent=Extent.from_items(items),
183
- description=collection.description,
184
- title=collection.title,
185
- stac_extensions=collection.stac_extensions,
186
- license=collection.license,
187
- keywords=collection.keywords,
188
- providers=collection.providers,
189
- summaries=collection.summaries,
190
- extra_fields=collection.extra_fields,
206
+ description=self.description,
207
+ title=self.client.title,
208
+ stac_extensions=self.stac_extensions,
209
+ license=self.client.license,
210
+ keywords=self.client.keywords,
211
+ providers=self.client.providers,
212
+ summaries=self.client.summaries,
213
+ extra_fields=self.client.extra_fields,
191
214
  catalog_type=CatalogType.SELF_CONTAINED,
192
215
  )
193
216
 
@@ -209,14 +232,17 @@ class StaticCatalogWriterMixin(CatalogSearcher):
209
232
 
210
233
  def filter_items(
211
234
  items: Generator[Item, None, None],
212
- cloud_cover_field: str = "eo:cloud_cover",
213
- max_cloud_cover: float = 100.0,
235
+ query: Optional[str] = None,
214
236
  ) -> Generator[Item, None, None]:
215
237
  """
216
238
  Only for cloudcover now, this can and should be adapted for filter field and value
217
239
  the field and value for the item filter would be defined in search.config.py corresponding configs
218
240
  and passed down to the individual search approaches via said config and this Function.
219
241
  """
220
- for item in items:
221
- if item.properties.get(cloud_cover_field, 0.0) <= max_cloud_cover:
222
- yield item
242
+ if query:
243
+ expr = Expr(query)
244
+ for item in items:
245
+ if expr.matches(item.properties):
246
+ yield item
247
+ else:
248
+ yield from items
@@ -1,23 +1,44 @@
1
- from typing import Optional
1
+ from typing import Optional, Dict, Any
2
2
 
3
3
  from mapchete.path import MPath, MPathLike
4
- from pydantic import BaseModel
4
+ from pydantic import BaseModel, model_validator
5
5
 
6
6
 
7
7
  class StacSearchConfig(BaseModel):
8
8
  max_cloud_cover: float = 100.0
9
+ query: Optional[str] = None
9
10
  catalog_chunk_threshold: int = 10_000
10
11
  catalog_chunk_zoom: int = 5
11
12
  catalog_pagesize: int = 100
12
13
  footprint_buffer: float = 0
13
14
 
15
+ @model_validator(mode="before")
16
+ def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
17
+ if "max_cloud_cover" in values: # pragma: no cover
18
+ raise DeprecationWarning(
19
+ "'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
20
+ )
21
+ return values
22
+
14
23
 
15
24
  class StacStaticConfig(BaseModel):
16
- max_cloud_cover: float = 100.0
25
+ @model_validator(mode="before")
26
+ def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
27
+ if "max_cloud_cover" in values: # pragma: no cover
28
+ raise DeprecationWarning(
29
+ "'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
30
+ )
31
+ return values
17
32
 
18
33
 
19
34
  class UTMSearchConfig(BaseModel):
20
- max_cloud_cover: float = 100.0
35
+ @model_validator(mode="before")
36
+ def deprecate_max_cloud_cover(cls, values: Dict[str, Any]) -> Dict[str, Any]:
37
+ if "max_cloud_cover" in values: # pragma: no cover
38
+ raise DeprecationWarning(
39
+ "'max_cloud_cover' will be deprecated soon. Please use 'eo:cloud_cover<=...' in the source 'query' field.",
40
+ )
41
+ return values
21
42
 
22
43
  sinergise_aws_collections: dict = dict(
23
44
  S2_L2A=dict(
@@ -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 reproject_geometry
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 = bounds_to_geom(bounds)
293
+ aoi = bounds.latlon_geometry()
295
294
  prepare(aoi)
296
295
 
297
296
  def tiles_generator():