rio-tiler 7.9.1__py3-none-any.whl → 8.0.0__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.
rio_tiler/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """rio-tiler."""
2
2
 
3
- __version__ = "7.9.1"
3
+ __version__ = "8.0.0"
4
4
 
5
5
  from . import ( # noqa
6
6
  colormap,
rio_tiler/colormap.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """rio-tiler colormap functions and classes."""
2
2
 
3
+ import itertools
3
4
  import json
4
5
  import os
5
6
  import pathlib
@@ -166,9 +167,10 @@ def apply_discrete_cmap(
166
167
 
167
168
  data = numpy.transpose(res, [2, 0, 1])
168
169
 
169
- # If the output data has values between 0-255
170
+ # If colormap values are between 0-255
170
171
  # we cast the output array to Uint8
171
- if data.min() >= 0 and data.max() <= 255:
172
+ cmap_v = list(itertools.chain(*colormap.values()))
173
+ if min(cmap_v) >= 0 and max(cmap_v) <= 255:
172
174
  data = data.astype("uint8")
173
175
 
174
176
  return data[:-1], data[-1]
@@ -206,9 +208,10 @@ def apply_intervals_cmap(
206
208
 
207
209
  data = numpy.transpose(res, [2, 0, 1])
208
210
 
209
- # If the output data has values between 0-255
211
+ # If colormap values are between 0-255
210
212
  # we cast the output array to Uint8
211
- if data.min() >= 0 and data.max() <= 255:
213
+ cmap_v = list(itertools.chain(*[v for k, v in colormap]))
214
+ if min(cmap_v) >= 0 and max(cmap_v) <= 255:
212
215
  data = data.astype("uint8")
213
216
 
214
217
  return data[:-1], data[-1]
rio_tiler/errors.py CHANGED
@@ -95,3 +95,11 @@ class InvalidGeographicBounds(RioTilerError):
95
95
 
96
96
  class RioTilerExperimentalWarning(UserWarning):
97
97
  """A rio-tiler specific experimental functionality warning."""
98
+
99
+
100
+ class MaxArraySizeError(RioTilerError):
101
+ """Trying to load to many pixels in memory."""
102
+
103
+
104
+ class NoAssetFoundError(RioTilerError):
105
+ """No Asset found"""
@@ -0,0 +1,366 @@
1
+ """rio_tiler experimental ZarrReaders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from functools import cache
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, List, Literal, Optional, Union
9
+ from urllib.parse import urlparse
10
+
11
+ import attr
12
+ from morecantile import TileMatrixSet
13
+ from rasterio.crs import CRS
14
+
15
+ from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
16
+ from rio_tiler.errors import InvalidGeographicBounds
17
+ from rio_tiler.io.base import BaseReader
18
+ from rio_tiler.io.xarray import XarrayReader
19
+ from rio_tiler.models import BandStatistics, ImageData, Info, PointData
20
+ from rio_tiler.types import BBox
21
+
22
+ try:
23
+ import obstore
24
+ from zarr.storage import ObjectStore
25
+ except ImportError: # pragma: nocover
26
+ ObjectStore = None # type: ignore
27
+ obstore = None # type: ignore
28
+
29
+ try:
30
+ import rioxarray
31
+ import xarray
32
+ except ImportError: # pragma: nocover
33
+ xarray = None # type: ignore
34
+ rioxarray = None # type: ignore
35
+
36
+ sel_methods = Literal["nearest", "pad", "ffill", "backfill", "bfill"]
37
+
38
+
39
+ @cache
40
+ def open_dataset(src_path: str, **kwargs: Any) -> xarray.Dataset:
41
+ """Open Xarray dataset
42
+
43
+ Args:
44
+ src_path (str): dataset path.
45
+
46
+ Returns:
47
+ xarray.DataTree
48
+
49
+ """
50
+ parsed = urlparse(src_path)
51
+ if not parsed.scheme:
52
+ src_path = str(Path(src_path).resolve())
53
+ src_path = "file://" + src_path
54
+ store = obstore.store.from_url(src_path, **kwargs)
55
+ zarr_store = ObjectStore(store=store, read_only=True)
56
+ ds = xarray.open_dataset(
57
+ zarr_store,
58
+ decode_times=True,
59
+ decode_coords="all",
60
+ consolidated=True,
61
+ engine="zarr",
62
+ )
63
+ return ds
64
+
65
+
66
+ @attr.s
67
+ class ZarrReader(BaseReader):
68
+ """Zarr dataset Reader.
69
+
70
+ Attributes:
71
+ input (str): dataset path.
72
+ dataset (xarray.Dataset): Xarray dataset.
73
+ tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`.
74
+ opener (Callable): Xarray dataset opener. Defaults to `open_dataset`.
75
+ opener_options (dict): Options to forward to the opener callable.
76
+
77
+ Examples:
78
+ >>> with ZarrReader(
79
+ "s3://mur-sst/zarr-v1",
80
+ opener_options={
81
+ "skip_signature": True,
82
+ "region": "us-west-2",
83
+ }
84
+ ) as src:
85
+ print(src)
86
+ print(src.variables)
87
+ img = src.tile(x, y, z, tmax)
88
+
89
+ """
90
+
91
+ input: str = attr.ib()
92
+ dataset: xarray.Dataset = attr.ib(default=None)
93
+
94
+ tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
95
+
96
+ opener: Callable[..., xarray.Dataset] = attr.ib(default=open_dataset)
97
+ opener_options: Dict = attr.ib(factory=dict)
98
+ _ctx_stack: contextlib.ExitStack = attr.ib(init=False, factory=contextlib.ExitStack)
99
+
100
+ def __attrs_post_init__(self):
101
+ """Set bounds and CRS."""
102
+ assert xarray is not None, "xarray must be installed to use XarrayReader"
103
+ assert rioxarray is not None, "rioxarray must be installed to use XarrayReader"
104
+
105
+ if not self.dataset:
106
+ self.dataset = self._ctx_stack.enter_context(
107
+ self.opener(self.input, **self.opener_options)
108
+ )
109
+
110
+ # NOTE: rioxarray returns **ordered** bounds in form of (minx, miny, maxx, maxx)
111
+ self.bounds = tuple(self.dataset.rio.bounds())
112
+ # Make sure we have a valid CRS
113
+ self.dataset = self.dataset.rio.write_crs(
114
+ self.dataset.rio.crs or "epsg:4326",
115
+ )
116
+
117
+ self.crs = self.dataset.rio.crs
118
+
119
+ # adds half x/y resolution on each values
120
+ # https://github.com/corteva/rioxarray/issues/645#issuecomment-1461070634
121
+ xres, yres = map(abs, self.dataset.rio.resolution())
122
+ if self.crs == WGS84_CRS and (
123
+ self.bounds[0] + xres / 2 < -180
124
+ or self.bounds[1] + yres / 2 < -90
125
+ or self.bounds[2] - xres / 2 > 180
126
+ or self.bounds[3] - yres / 2 > 90
127
+ ):
128
+ raise InvalidGeographicBounds(
129
+ f"Invalid geographic bounds: {self.bounds}. Must be within (-180, -90, 180, 90)."
130
+ )
131
+
132
+ self.transform = self.dataset.rio.transform()
133
+ self.height = self.dataset.rio.height
134
+ self.width = self.dataset.rio.width
135
+
136
+ def close(self):
137
+ """Close xarray dataset."""
138
+ self._ctx_stack.close()
139
+
140
+ def __exit__(self, exc_type, exc_value, traceback):
141
+ """Support using with Context Managers."""
142
+ self.close()
143
+
144
+ @property
145
+ def variables(self) -> List[str]:
146
+ """Return dataset variable names"""
147
+ return list(self.dataset.data_vars)
148
+
149
+ def _arrange_dims(self, da: xarray.DataArray) -> xarray.DataArray:
150
+ """Arrange coordinates and time dimensions.
151
+
152
+ An rioxarray.exceptions.InvalidDimensionOrder error is raised if the coordinates are not in the correct order time, y, and x.
153
+ See: https://github.com/corteva/rioxarray/discussions/674
154
+
155
+ We conform to using x and y as the spatial dimension names..
156
+
157
+ """
158
+ if "x" not in da.dims and "y" not in da.dims:
159
+ try:
160
+ latitude_var_name = next(
161
+ name
162
+ for name in ["lat", "latitude", "LAT", "LATITUDE", "Lat"]
163
+ if name in da.dims
164
+ )
165
+ longitude_var_name = next(
166
+ name
167
+ for name in ["lon", "longitude", "LON", "LONGITUDE", "Lon"]
168
+ if name in da.dims
169
+ )
170
+ except StopIteration as e:
171
+ raise ValueError(f"Couldn't find X/Y dimensions in {da.dims}") from e
172
+
173
+ da = da.rename({latitude_var_name: "y", longitude_var_name: "x"})
174
+
175
+ if "TIME" in da.dims:
176
+ da = da.rename({"TIME": "time"})
177
+
178
+ if extra_dims := [d for d in da.dims if d not in ["x", "y"]]:
179
+ da = da.transpose(*extra_dims, "y", "x")
180
+ else:
181
+ da = da.transpose("y", "x")
182
+
183
+ # If min/max values are stored in `valid_range` we add them in `valid_min/valid_max`
184
+ vmin, vmax = da.attrs.get("valid_min"), da.attrs.get("valid_max")
185
+ if "valid_range" in da.attrs and not (vmin is not None and vmax is not None):
186
+ valid_range = da.attrs.get("valid_range")
187
+ da.attrs.update({"valid_min": valid_range[0], "valid_max": valid_range[1]})
188
+
189
+ return da
190
+
191
+ def _get_variable(
192
+ self,
193
+ variable: str,
194
+ sel: Optional[List[str]] = None,
195
+ method: Optional[sel_methods] = None,
196
+ ) -> xarray.DataArray:
197
+ """Get DataArray from xarray Dataset."""
198
+ da = self.dataset[variable]
199
+
200
+ if sel:
201
+ _idx: Dict[str, List] = {}
202
+ for s in sel:
203
+ val: Union[str, slice]
204
+ dim, val = s.split("=")
205
+
206
+ # cast string to dtype of the dimension
207
+ if da[dim].dtype != "O":
208
+ val = da[dim].dtype.type(val)
209
+
210
+ if dim in _idx:
211
+ _idx[dim].append(val)
212
+ else:
213
+ _idx[dim] = [val]
214
+
215
+ sel_idx = {k: v[0] if len(v) < 2 else v for k, v in _idx.items()}
216
+ da = da.sel(sel_idx, method=method)
217
+
218
+ da = self._arrange_dims(da)
219
+
220
+ # Make sure we have a valid CRS
221
+ crs = da.rio.crs or "epsg:4326"
222
+ da = da.rio.write_crs(crs)
223
+
224
+ if crs == "epsg:4326" and (da.x > 180).any():
225
+ # Adjust the longitude coordinates to the -180 to 180 range
226
+ da = da.assign_coords(x=(da.x + 180) % 360 - 180)
227
+
228
+ # Sort the dataset by the updated longitude coordinates
229
+ da = da.sortby(da.x)
230
+
231
+ assert len(da.dims) in [
232
+ 2,
233
+ 3,
234
+ ], "rio_tiler.io.xarray.DatasetReader can only work with 2D or 3D DataArray"
235
+
236
+ return da
237
+
238
+ def spatial_info( # type: ignore
239
+ self,
240
+ *,
241
+ variable: str,
242
+ sel: Optional[List[str]] = None,
243
+ method: Optional[sel_methods] = None,
244
+ ):
245
+ """Return xarray.DataArray info."""
246
+ with XarrayReader(
247
+ self._get_variable(variable, sel=sel, method=method),
248
+ ) as da:
249
+ return {
250
+ "crs": da.crs,
251
+ "bounds": da.bounds,
252
+ "minzoom": da.minzoom,
253
+ "maxzoom": da.maxzoom,
254
+ }
255
+
256
+ def get_geographic_bounds( # type: ignore
257
+ self,
258
+ crs: CRS,
259
+ *,
260
+ variable: str,
261
+ sel: Optional[List[str]] = None,
262
+ method: Optional[sel_methods] = None,
263
+ ) -> BBox:
264
+ """Return Geographic Bounds for a Geographic CRS."""
265
+ with XarrayReader(
266
+ self._get_variable(variable, sel=sel, method=method),
267
+ ) as da:
268
+ return da.get_geographic_bounds(crs)
269
+
270
+ def info( # type: ignore
271
+ self,
272
+ *,
273
+ variable: str,
274
+ sel: Optional[List[str]] = None,
275
+ method: Optional[sel_methods] = None,
276
+ ) -> Info:
277
+ """Return xarray.DataArray info."""
278
+ with XarrayReader(
279
+ self._get_variable(variable, sel=sel, method=method),
280
+ ) as da:
281
+ return da.info()
282
+
283
+ def statistics( # type: ignore
284
+ self,
285
+ *args: Any,
286
+ variable: str,
287
+ sel: Optional[List[str]] = None,
288
+ method: Optional[sel_methods] = None,
289
+ **kwargs: Any,
290
+ ) -> Dict[str, BandStatistics]:
291
+ """Return statistics from a dataset."""
292
+ with XarrayReader(
293
+ self._get_variable(variable, sel=sel, method=method),
294
+ ) as da:
295
+ return da.statistics(*args, **kwargs)
296
+
297
+ def tile( # type: ignore
298
+ self,
299
+ *args: Any,
300
+ variable: str,
301
+ sel: Optional[List[str]] = None,
302
+ method: Optional[sel_methods] = None,
303
+ **kwargs: Any,
304
+ ) -> ImageData:
305
+ """Read a Web Map tile from a dataset."""
306
+ with XarrayReader(
307
+ self._get_variable(variable, sel=sel, method=method),
308
+ tms=self.tms,
309
+ ) as da:
310
+ return da.tile(*args, **kwargs)
311
+
312
+ def part( # type: ignore
313
+ self,
314
+ *args: Any,
315
+ variable: str,
316
+ sel: Optional[List[str]] = None,
317
+ method: Optional[sel_methods] = None,
318
+ **kwargs: Any,
319
+ ) -> ImageData:
320
+ """Read part of a dataset."""
321
+ with XarrayReader(
322
+ self._get_variable(variable, sel=sel, method=method),
323
+ ) as da:
324
+ return da.part(*args, **kwargs)
325
+
326
+ def preview( # type: ignore
327
+ self,
328
+ *args: Any,
329
+ variable: str,
330
+ sel: Optional[List[str]] = None,
331
+ method: Optional[sel_methods] = None,
332
+ **kwargs: Any,
333
+ ) -> ImageData:
334
+ """Return a preview of a dataset."""
335
+ with XarrayReader(
336
+ self._get_variable(variable, sel=sel, method=method),
337
+ ) as da:
338
+ return da.preview(*args, **kwargs)
339
+
340
+ def point( # type: ignore
341
+ self,
342
+ *args: Any,
343
+ variable: str,
344
+ sel: Optional[List[str]] = None,
345
+ method: Optional[sel_methods] = None,
346
+ **kwargs: Any,
347
+ ) -> PointData:
348
+ """Read a pixel value from a dataset."""
349
+ with XarrayReader(
350
+ self._get_variable(variable, sel=sel, method=method),
351
+ ) as da:
352
+ return da.point(*args, **kwargs)
353
+
354
+ def feature( # type: ignore
355
+ self,
356
+ *args: Any,
357
+ variable: str,
358
+ sel: Optional[List[str]] = None,
359
+ method: Optional[sel_methods] = None,
360
+ **kwargs: Any,
361
+ ) -> ImageData:
362
+ """Read part of a dataset defined by a geojson feature."""
363
+ with XarrayReader(
364
+ self._get_variable(variable, sel=sel, method=method),
365
+ ) as da:
366
+ return da.feature(*args, **kwargs)
rio_tiler/expression.py CHANGED
@@ -10,7 +10,7 @@ from rio_tiler.errors import InvalidExpression
10
10
 
11
11
 
12
12
  def parse_expression(expression: str, cast: bool = True) -> Tuple:
13
- """Parse rio-tiler band math expression.
13
+ """Parse rio-tiler band math expression and extract bands.
14
14
 
15
15
  Args:
16
16
  expression (str): band math/combination expression.
@@ -20,11 +20,11 @@ def parse_expression(expression: str, cast: bool = True) -> Tuple:
20
20
  tuple: band names/indexes.
21
21
 
22
22
  Examples:
23
- >>> parse_expression("b1;b2")
24
- (2, 1)
23
+ >>> parse_expression("b1+b2")
24
+ (1, 2)
25
25
 
26
26
  >>> parse_expression("B1/B2", cast=False)
27
- ("2", "1")
27
+ ('1', '2')
28
28
 
29
29
  """
30
30
  bands = set(re.findall(r"\bb(?P<bands>[0-9A-Z]+)\b", expression, re.IGNORECASE))
@@ -47,8 +47,8 @@ def get_expression_blocks(expression: str) -> List[str]:
47
47
  list: expression blocks (str).
48
48
 
49
49
  Examples:
50
- >>> parse_expression("b1/b2,b2+b1")
51
- ("b1/b2", "b2+b1")
50
+ >>> get_expression_blocks("b1/b2;b2+b1")
51
+ ['b1/b2', 'b2+b1']
52
52
 
53
53
  """
54
54
  return [expr for expr in expression.split(";") if expr]
rio_tiler/io/base.py CHANGED
@@ -620,8 +620,12 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
620
620
  "Can't use `asset_as_band` for multibands asset"
621
621
  )
622
622
  data.band_names = [asset]
623
+ data.band_descriptions = [asset]
623
624
  else:
624
625
  data.band_names = [f"{asset}_{n}" for n in data.band_names]
626
+ data.band_descriptions = [
627
+ f"{asset}_{n}" for n in data.band_descriptions
628
+ ]
625
629
 
626
630
  return data
627
631
 
@@ -711,8 +715,12 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
711
715
  "Can't use `asset_as_band` for multibands asset"
712
716
  )
713
717
  data.band_names = [asset]
718
+ data.band_descriptions = [asset]
714
719
  else:
715
720
  data.band_names = [f"{asset}_{n}" for n in data.band_names]
721
+ data.band_descriptions = [
722
+ f"{asset}_{n}" for n in data.band_descriptions
723
+ ]
716
724
 
717
725
  return data
718
726
 
@@ -800,8 +808,12 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
800
808
  "Can't use `asset_as_band` for multibands asset"
801
809
  )
802
810
  data.band_names = [asset]
811
+ data.band_descriptions = [asset]
803
812
  else:
804
813
  data.band_names = [f"{asset}_{n}" for n in data.band_names]
814
+ data.band_descriptions = [
815
+ f"{asset}_{n}" for n in data.band_descriptions
816
+ ]
805
817
 
806
818
  return data
807
819
 
@@ -887,8 +899,12 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
887
899
  "Can't use `asset_as_band` for multibands asset"
888
900
  )
889
901
  data.band_names = [asset]
902
+ data.band_descriptions = [asset]
890
903
  else:
891
904
  data.band_names = [f"{asset}_{n}" for n in data.band_names]
905
+ data.band_descriptions = [
906
+ f"{asset}_{n}" for n in data.band_descriptions
907
+ ]
892
908
 
893
909
  return data
894
910
 
@@ -978,8 +994,12 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
978
994
  "Can't use `asset_as_band` for multibands asset"
979
995
  )
980
996
  data.band_names = [asset]
997
+ data.band_descriptions = [asset]
981
998
  else:
982
999
  data.band_names = [f"{asset}_{n}" for n in data.band_names]
1000
+ data.band_descriptions = [
1001
+ f"{asset}_{n}" for n in data.band_descriptions
1002
+ ]
983
1003
 
984
1004
  return data
985
1005
 
rio_tiler/io/rasterio.py CHANGED
@@ -637,7 +637,6 @@ class ImageReader(Reader):
637
637
  tilesize: int = 256,
638
638
  indexes: Optional[Indexes] = None,
639
639
  expression: Optional[str] = None,
640
- force_binary_mask: bool = True,
641
640
  out_dtype: Optional[Union[str, numpy.dtype]] = None,
642
641
  resampling_method: RIOResampling = "nearest",
643
642
  unscale: bool = False,
@@ -654,7 +653,6 @@ class ImageReader(Reader):
654
653
  tilesize (int, optional): Output image size. Defaults to `256`.
655
654
  indexes (int or sequence of int, optional): Band indexes.
656
655
  expression (str, optional): rio-tiler expression (e.g. b1/b2+b3).
657
- force_binary_mask (bool, optional): Cast returned mask to binary values (0 or 255). Defaults to `True`.
658
656
  resampling_method (RIOResampling, optional): RasterIO resampling algorithm. Defaults to `nearest`.
659
657
  unscale (bool, optional): Apply 'scales' and 'offsets' on output data value. Defaults to `False`.
660
658
  post_process (callable, optional): Function to apply on output data and mask values.
@@ -675,7 +673,6 @@ class ImageReader(Reader):
675
673
  max_size=None,
676
674
  indexes=indexes,
677
675
  expression=expression,
678
- force_binary_mask=force_binary_mask,
679
676
  out_dtype=out_dtype,
680
677
  resampling_method=resampling_method,
681
678
  unscale=unscale,
@@ -690,7 +687,6 @@ class ImageReader(Reader):
690
687
  max_size: Optional[int] = None,
691
688
  height: Optional[int] = None,
692
689
  width: Optional[int] = None,
693
- force_binary_mask: bool = True,
694
690
  out_dtype: Optional[Union[str, numpy.dtype]] = None,
695
691
  resampling_method: RIOResampling = "nearest",
696
692
  unscale: bool = False,
@@ -707,7 +703,6 @@ class ImageReader(Reader):
707
703
  max_size (int, optional): Limit the size of the longest dimension of the dataset read, respecting bounds X/Y aspect ratio.
708
704
  height (int, optional): Output height of the array.
709
705
  width (int, optional): Output width of the array.
710
- force_binary_mask (bool, optional): Cast returned mask to binary values (0 or 255). Defaults to `True`.
711
706
  resampling_method (RIOResampling, optional): RasterIO resampling algorithm. Defaults to `nearest`.
712
707
  unscale (bool, optional): Apply 'scales' and 'offsets' on output data value. Defaults to `False`.
713
708
  post_process (callable, optional): Function to apply on output data and mask values.
@@ -733,7 +728,6 @@ class ImageReader(Reader):
733
728
  width=width,
734
729
  height=height,
735
730
  indexes=indexes,
736
- force_binary_mask=force_binary_mask,
737
731
  out_dtype=out_dtype,
738
732
  resampling_method=resampling_method,
739
733
  unscale=unscale,
@@ -793,6 +787,7 @@ class ImageReader(Reader):
793
787
  coordinates=self.dataset.xy(x, y),
794
788
  crs=self.dataset.crs,
795
789
  band_names=img.band_names,
790
+ band_descriptions=img.band_descriptions,
796
791
  pixel_location=(x, y),
797
792
  )
798
793
 
@@ -804,7 +799,6 @@ class ImageReader(Reader):
804
799
  max_size: Optional[int] = None,
805
800
  height: Optional[int] = None,
806
801
  width: Optional[int] = None,
807
- force_binary_mask: bool = True,
808
802
  out_dtype: Optional[Union[str, numpy.dtype]] = None,
809
803
  resampling_method: RIOResampling = "nearest",
810
804
  unscale: bool = False,
@@ -824,7 +818,6 @@ class ImageReader(Reader):
824
818
  max_size=max_size,
825
819
  height=height,
826
820
  width=width,
827
- force_binary_mask=force_binary_mask,
828
821
  out_dtype=out_dtype,
829
822
  resampling_method=resampling_method,
830
823
  unscale=unscale,