rio-tiler 6.8.0__py3-none-any.whl → 7.0.1__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/io/base.py CHANGED
@@ -9,11 +9,13 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
9
9
 
10
10
  import attr
11
11
  import numpy
12
+ from affine import Affine
12
13
  from morecantile import Tile, TileMatrixSet
13
14
  from rasterio.crs import CRS
14
- from rasterio.warp import transform_bounds
15
+ from rasterio.rio.overview import get_maximum_overview_level
16
+ from rasterio.warp import calculate_default_transform, transform_bounds
15
17
 
16
- from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
18
+ from rio_tiler.constants import WEB_MERCATOR_TMS
17
19
  from rio_tiler.errors import (
18
20
  AssetAsBandError,
19
21
  ExpressionMixingWarning,
@@ -25,7 +27,7 @@ from rio_tiler.errors import (
25
27
  from rio_tiler.models import BandStatistics, ImageData, Info, PointData
26
28
  from rio_tiler.tasks import multi_arrays, multi_points, multi_values
27
29
  from rio_tiler.types import AssetInfo, BBox, Indexes
28
- from rio_tiler.utils import normalize_bounds
30
+ from rio_tiler.utils import CRS_to_uri, cast_to_sequence, normalize_bounds
29
31
 
30
32
 
31
33
  @attr.s
@@ -42,12 +44,13 @@ class SpatialMixin:
42
44
  bounds: BBox = attr.ib(init=False)
43
45
  crs: CRS = attr.ib(init=False)
44
46
 
45
- geographic_crs: CRS = attr.ib(init=False, default=WGS84_CRS)
47
+ transform: Optional[Affine] = attr.ib(default=None, init=False)
48
+ height: Optional[int] = attr.ib(default=None, init=False)
49
+ width: Optional[int] = attr.ib(default=None, init=False)
46
50
 
47
- @cached_property
48
- def geographic_bounds(self) -> BBox:
49
- """Return dataset bounds in geographic_crs."""
50
- if self.crs == self.geographic_crs:
51
+ def get_geographic_bounds(self, crs: CRS) -> BBox:
52
+ """Return Geographic Bounds for a Geographic CRS."""
53
+ if self.crs == crs:
51
54
  if self.bounds[1] > self.bounds[3]:
52
55
  warnings.warn(
53
56
  "BoundingBox of the dataset is inverted (minLat > maxLat).",
@@ -63,12 +66,7 @@ class SpatialMixin:
63
66
  return self.bounds
64
67
 
65
68
  try:
66
- bounds = transform_bounds(
67
- self.crs,
68
- self.geographic_crs,
69
- *self.bounds,
70
- densify_pts=21,
71
- )
69
+ bounds = transform_bounds(self.crs, crs, *self.bounds, densify_pts=21)
72
70
  except: # noqa
73
71
  warnings.warn(
74
72
  "Cannot determine bounds in geographic CRS, will default to (-180.0, -90.0, 180.0, 90.0).",
@@ -85,6 +83,77 @@ class SpatialMixin:
85
83
 
86
84
  return bounds
87
85
 
86
+ @cached_property
87
+ def _dst_geom_in_tms_crs(self):
88
+ """Return dataset geom info in TMS projection."""
89
+ tms_crs = self.tms.rasterio_crs
90
+ if self.crs != tms_crs:
91
+ dst_affine, w, h = calculate_default_transform(
92
+ self.crs,
93
+ tms_crs,
94
+ self.width,
95
+ self.height,
96
+ *self.bounds,
97
+ )
98
+ else:
99
+ dst_affine = list(self.transform)
100
+ w = self.width
101
+ h = self.height
102
+
103
+ return dst_affine, w, h
104
+
105
+ @cached_property
106
+ def _minzoom(self) -> int:
107
+ """Calculate dataset minimum zoom level."""
108
+ # We assume the TMS tilesize to be constant over all matrices
109
+ # ref: https://github.com/OSGeo/gdal/blob/dc38aa64d779ecc45e3cd15b1817b83216cf96b8/gdal/frmts/gtiff/cogdriver.cpp#L274
110
+ tilesize = self.tms.tileMatrices[0].tileWidth
111
+
112
+ if all([self.transform, self.height, self.width]):
113
+ try:
114
+ dst_affine, w, h = self._dst_geom_in_tms_crs
115
+
116
+ # The minzoom is defined by the resolution of the maximum theoretical overview level
117
+ # We assume `tilesize`` is the smallest overview size
118
+ overview_level = get_maximum_overview_level(w, h, minsize=tilesize)
119
+
120
+ # Get the resolution of the overview
121
+ resolution = max(abs(dst_affine[0]), abs(dst_affine[4]))
122
+ ovr_resolution = resolution * (2**overview_level)
123
+
124
+ # Find what TMS matrix match the overview resolution
125
+ return self.tms.zoom_for_res(ovr_resolution)
126
+
127
+ except: # noqa
128
+ # if we can't get max zoom from the dataset we default to TMS maxzoom
129
+ warnings.warn(
130
+ "Cannot determine minzoom based on dataset information, will default to TMS minzoom.",
131
+ UserWarning,
132
+ )
133
+
134
+ return self.tms.minzoom
135
+
136
+ @cached_property
137
+ def _maxzoom(self) -> int:
138
+ """Calculate dataset maximum zoom level."""
139
+ if all([self.transform, self.height, self.width]):
140
+ try:
141
+ dst_affine, _, _ = self._dst_geom_in_tms_crs
142
+
143
+ # The maxzoom is defined by finding the minimum difference between
144
+ # the raster resolution and the zoom level resolution
145
+ resolution = max(abs(dst_affine[0]), abs(dst_affine[4]))
146
+ return self.tms.zoom_for_res(resolution)
147
+
148
+ except: # noqa
149
+ # if we can't get min/max zoom from the dataset we default to TMS maxzoom
150
+ warnings.warn(
151
+ "Cannot determine maxzoom based on dataset information, will default to TMS maxzoom.",
152
+ UserWarning,
153
+ )
154
+
155
+ return self.tms.maxzoom
156
+
88
157
  def tile_exists(self, tile_x: int, tile_y: int, tile_z: int) -> bool:
89
158
  """Check if a tile intersects the dataset bounds.
90
159
 
@@ -98,7 +167,7 @@ class SpatialMixin:
98
167
 
99
168
  """
100
169
  # bounds in TileMatrixSet's CRS
101
- tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z))
170
+ tile_bounds = tuple(self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)))
102
171
 
103
172
  if not self.tms.rasterio_crs == self.crs:
104
173
  # Transform the bounds to the dataset's CRS
@@ -267,8 +336,11 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
267
336
  reader_options: Dict = attr.ib(factory=dict)
268
337
 
269
338
  assets: Sequence[str] = attr.ib(init=False)
339
+ default_assets: Optional[Sequence[str]] = attr.ib(init=False, default=None)
270
340
 
271
- ctx: Any = attr.ib(init=False, default=contextlib.nullcontext)
341
+ ctx: Type[contextlib.AbstractContextManager] = attr.ib(
342
+ init=False, default=contextlib.nullcontext
343
+ )
272
344
 
273
345
  def __enter__(self):
274
346
  """Support using with Context Managers."""
@@ -283,6 +355,10 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
283
355
  """Validate asset name and construct url."""
284
356
  ...
285
357
 
358
+ def _get_reader(self, asset_info: AssetInfo) -> Tuple[Type[BaseReader], Dict]:
359
+ """Get Asset Reader and options."""
360
+ return self.reader, {}
361
+
286
362
  def parse_expression(self, expression: str, asset_as_band: bool = False) -> Tuple:
287
363
  """Parse rio-tiler band math expression."""
288
364
  input_assets = "|".join(self.assets)
@@ -309,8 +385,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
309
385
  statistics: Optional[Sequence[Tuple[float, float]]] = None,
310
386
  ):
311
387
  """Update ImageData Statistics from AssetInfo."""
312
- if isinstance(indexes, int):
313
- indexes = (indexes,)
388
+ indexes = cast_to_sequence(indexes)
314
389
 
315
390
  if indexes is None:
316
391
  indexes = tuple(range(1, img.count + 1))
@@ -322,7 +397,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
322
397
  img.dataset_statistics = [statistics[bidx - 1] for bidx in indexes]
323
398
 
324
399
  def info(
325
- self, assets: Union[Sequence[str], str] = None, **kwargs: Any
400
+ self,
401
+ assets: Optional[Union[Sequence[str], str]] = None,
402
+ **kwargs: Any,
326
403
  ) -> Dict[str, Info]:
327
404
  """Return metadata from multiple assets.
328
405
 
@@ -338,26 +415,27 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
338
415
  "No `assets` option passed, will fetch info for all available assets.",
339
416
  UserWarning,
340
417
  )
341
-
342
- assets = assets or self.assets
343
-
344
- if isinstance(assets, str):
345
- assets = (assets,)
418
+ assets = cast_to_sequence(assets or self.assets)
346
419
 
347
420
  def _reader(asset: str, **kwargs: Any) -> Dict:
348
421
  asset_info = self._get_asset_info(asset)
349
- url = asset_info["url"]
422
+ reader, options = self._get_reader(asset_info)
423
+
350
424
  with self.ctx(**asset_info.get("env", {})):
351
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
425
+ with reader(
426
+ asset_info["url"],
427
+ tms=self.tms,
428
+ **{**self.reader_options, **options},
429
+ ) as src:
352
430
  return src.info()
353
431
 
354
432
  return multi_values(assets, _reader, **kwargs)
355
433
 
356
434
  def statistics(
357
435
  self,
358
- assets: Union[Sequence[str], str] = None,
359
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
360
- asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset
436
+ assets: Optional[Union[Sequence[str], str]] = None,
437
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
438
+ asset_expression: Optional[Dict[str, str]] = None,
361
439
  **kwargs: Any,
362
440
  ) -> Dict[str, Dict[str, BandStatistics]]:
363
441
  """Return array statistics for multiple assets.
@@ -378,23 +456,24 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
378
456
  UserWarning,
379
457
  )
380
458
 
381
- assets = assets or self.assets
382
-
383
- if isinstance(assets, str):
384
- assets = (assets,)
385
-
459
+ assets = cast_to_sequence(assets or self.assets)
386
460
  asset_indexes = asset_indexes or {}
387
461
  asset_expression = asset_expression or {}
388
462
 
389
- def _reader(asset: str, *args, **kwargs) -> Dict:
463
+ def _reader(asset: str, *args: Any, **kwargs: Any) -> Dict:
390
464
  asset_info = self._get_asset_info(asset)
391
- url = asset_info["url"]
465
+ reader, options = self._get_reader(asset_info)
466
+
392
467
  with self.ctx(**asset_info.get("env", {})):
393
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
468
+ with reader(
469
+ asset_info["url"],
470
+ tms=self.tms,
471
+ **{**self.reader_options, **options},
472
+ ) as src:
394
473
  return src.statistics(
395
474
  *args,
396
- indexes=asset_indexes.get(asset, kwargs.pop("indexes", None)), # type: ignore
397
- expression=asset_expression.get(asset), # type: ignore
475
+ indexes=asset_indexes.get(asset, kwargs.pop("indexes", None)),
476
+ expression=asset_expression.get(asset),
398
477
  **kwargs,
399
478
  )
400
479
 
@@ -402,9 +481,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
402
481
 
403
482
  def merged_statistics(
404
483
  self,
405
- assets: Union[Sequence[str], str] = None,
484
+ assets: Optional[Union[Sequence[str], str]] = None,
406
485
  expression: Optional[str] = None,
407
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
486
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
408
487
  categorical: bool = False,
409
488
  categories: Optional[List[float]] = None,
410
489
  percentiles: Optional[List[int]] = None,
@@ -436,7 +515,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
436
515
  "No `assets` option passed, will fetch statistics for all available assets.",
437
516
  UserWarning,
438
517
  )
439
- assets = assets or self.assets
518
+ assets = cast_to_sequence(assets or self.assets)
440
519
 
441
520
  data = self.preview(
442
521
  assets=assets,
@@ -457,9 +536,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
457
536
  tile_x: int,
458
537
  tile_y: int,
459
538
  tile_z: int,
460
- assets: Union[Sequence[str], str] = None,
539
+ assets: Optional[Union[Sequence[str], str]] = None,
461
540
  expression: Optional[str] = None,
462
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
541
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
463
542
  asset_as_band: bool = False,
464
543
  **kwargs: Any,
465
544
  ) -> ImageData:
@@ -483,9 +562,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
483
562
  f"Tile(x={tile_x}, y={tile_y}, z={tile_z}) is outside bounds"
484
563
  )
485
564
 
486
- if isinstance(assets, str):
487
- assets = (assets,)
488
-
565
+ assets = cast_to_sequence(assets)
489
566
  if assets and expression:
490
567
  warnings.warn(
491
568
  "Both expression and assets passed; expression will overwrite assets parameter.",
@@ -495,9 +572,16 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
495
572
  if expression:
496
573
  assets = self.parse_expression(expression, asset_as_band=asset_as_band)
497
574
 
575
+ if not assets and self.default_assets:
576
+ warnings.warn(
577
+ f"No assets/expression passed, defaults to {self.default_assets}",
578
+ UserWarning,
579
+ )
580
+ assets = self.default_assets
581
+
498
582
  if not assets:
499
583
  raise MissingAssets(
500
- "assets must be passed either via `expression` or `assets` options."
584
+ "assets must be passed via `expression` or `assets` options, or via class-level `default_assets`."
501
585
  )
502
586
 
503
587
  asset_indexes = asset_indexes or {}
@@ -506,12 +590,17 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
506
590
  indexes = kwargs.pop("indexes", None)
507
591
 
508
592
  def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData:
509
- idx = asset_indexes.get(asset) or indexes # type: ignore
593
+ idx = asset_indexes.get(asset) or indexes
510
594
 
511
595
  asset_info = self._get_asset_info(asset)
512
- url = asset_info["url"]
596
+ reader, options = self._get_reader(asset_info)
597
+
513
598
  with self.ctx(**asset_info.get("env", {})):
514
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
599
+ with reader(
600
+ asset_info["url"],
601
+ tms=self.tms,
602
+ **{**self.reader_options, **options},
603
+ ) as src:
515
604
  data = src.tile(*args, indexes=idx, **kwargs)
516
605
 
517
606
  self._update_statistics(
@@ -545,9 +634,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
545
634
  def part(
546
635
  self,
547
636
  bbox: BBox,
548
- assets: Union[Sequence[str], str] = None,
637
+ assets: Optional[Union[Sequence[str], str]] = None,
549
638
  expression: Optional[str] = None,
550
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
639
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
551
640
  asset_as_band: bool = False,
552
641
  **kwargs: Any,
553
642
  ) -> ImageData:
@@ -564,9 +653,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
564
653
  rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info.
565
654
 
566
655
  """
567
- if isinstance(assets, str):
568
- assets = (assets,)
569
-
656
+ assets = cast_to_sequence(assets)
570
657
  if assets and expression:
571
658
  warnings.warn(
572
659
  "Both expression and assets passed; expression will overwrite assets parameter.",
@@ -576,9 +663,16 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
576
663
  if expression:
577
664
  assets = self.parse_expression(expression, asset_as_band=asset_as_band)
578
665
 
666
+ if not assets and self.default_assets:
667
+ warnings.warn(
668
+ f"No assets/expression passed, defaults to {self.default_assets}",
669
+ UserWarning,
670
+ )
671
+ assets = self.default_assets
672
+
579
673
  if not assets:
580
674
  raise MissingAssets(
581
- "assets must be passed either via `expression` or `assets` options."
675
+ "assets must be passed via `expression` or `assets` options, or via class-level `default_assets`."
582
676
  )
583
677
 
584
678
  asset_indexes = asset_indexes or {}
@@ -587,12 +681,17 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
587
681
  indexes = kwargs.pop("indexes", None)
588
682
 
589
683
  def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData:
590
- idx = asset_indexes.get(asset) or indexes # type: ignore
684
+ idx = asset_indexes.get(asset) or indexes
591
685
 
592
686
  asset_info = self._get_asset_info(asset)
593
- url = asset_info["url"]
687
+ reader, options = self._get_reader(asset_info)
688
+
594
689
  with self.ctx(**asset_info.get("env", {})):
595
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
690
+ with reader(
691
+ asset_info["url"],
692
+ tms=self.tms,
693
+ **{**self.reader_options, **options},
694
+ ) as src:
596
695
  data = src.part(*args, indexes=idx, **kwargs)
597
696
 
598
697
  self._update_statistics(
@@ -625,9 +724,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
625
724
 
626
725
  def preview(
627
726
  self,
628
- assets: Union[Sequence[str], str] = None,
727
+ assets: Optional[Union[Sequence[str], str]] = None,
629
728
  expression: Optional[str] = None,
630
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
729
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
631
730
  asset_as_band: bool = False,
632
731
  **kwargs: Any,
633
732
  ) -> ImageData:
@@ -643,9 +742,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
643
742
  rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info.
644
743
 
645
744
  """
646
- if isinstance(assets, str):
647
- assets = (assets,)
648
-
745
+ assets = cast_to_sequence(assets)
649
746
  if assets and expression:
650
747
  warnings.warn(
651
748
  "Both expression and assets passed; expression will overwrite assets parameter.",
@@ -655,9 +752,16 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
655
752
  if expression:
656
753
  assets = self.parse_expression(expression, asset_as_band=asset_as_band)
657
754
 
755
+ if not assets and self.default_assets:
756
+ warnings.warn(
757
+ f"No assets/expression passed, defaults to {self.default_assets}",
758
+ UserWarning,
759
+ )
760
+ assets = self.default_assets
761
+
658
762
  if not assets:
659
763
  raise MissingAssets(
660
- "assets must be passed either via `expression` or `assets` options."
764
+ "assets must be passed via `expression` or `assets` options, or via class-level `default_assets`."
661
765
  )
662
766
 
663
767
  asset_indexes = asset_indexes or {}
@@ -666,12 +770,17 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
666
770
  indexes = kwargs.pop("indexes", None)
667
771
 
668
772
  def _reader(asset: str, **kwargs: Any) -> ImageData:
669
- idx = asset_indexes.get(asset) or indexes # type: ignore
773
+ idx = asset_indexes.get(asset) or indexes
670
774
 
671
775
  asset_info = self._get_asset_info(asset)
672
- url = asset_info["url"]
776
+ reader, options = self._get_reader(asset_info)
777
+
673
778
  with self.ctx(**asset_info.get("env", {})):
674
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
779
+ with reader(
780
+ asset_info["url"],
781
+ tms=self.tms,
782
+ **{**self.reader_options, **options},
783
+ ) as src:
675
784
  data = src.preview(indexes=idx, **kwargs)
676
785
 
677
786
  self._update_statistics(
@@ -706,9 +815,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
706
815
  self,
707
816
  lon: float,
708
817
  lat: float,
709
- assets: Union[Sequence[str], str] = None,
818
+ assets: Optional[Union[Sequence[str], str]] = None,
710
819
  expression: Optional[str] = None,
711
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
820
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
712
821
  asset_as_band: bool = False,
713
822
  **kwargs: Any,
714
823
  ) -> PointData:
@@ -726,9 +835,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
726
835
  PointData
727
836
 
728
837
  """
729
- if isinstance(assets, str):
730
- assets = (assets,)
731
-
838
+ assets = cast_to_sequence(assets)
732
839
  if assets and expression:
733
840
  warnings.warn(
734
841
  "Both expression and assets passed; expression will overwrite assets parameter.",
@@ -738,9 +845,16 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
738
845
  if expression:
739
846
  assets = self.parse_expression(expression, asset_as_band=asset_as_band)
740
847
 
848
+ if not assets and self.default_assets:
849
+ warnings.warn(
850
+ f"No assets/expression passed, defaults to {self.default_assets}",
851
+ UserWarning,
852
+ )
853
+ assets = self.default_assets
854
+
741
855
  if not assets:
742
856
  raise MissingAssets(
743
- "assets must be passed either via `expression` or `assets` options."
857
+ "assets must be passed via `expression` or `assets` options, or via class-level `default_assets`."
744
858
  )
745
859
 
746
860
  asset_indexes = asset_indexes or {}
@@ -748,13 +862,18 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
748
862
  # We fall back to `indexes` if provided
749
863
  indexes = kwargs.pop("indexes", None)
750
864
 
751
- def _reader(asset: str, *args, **kwargs: Any) -> PointData:
752
- idx = asset_indexes.get(asset) or indexes # type: ignore
865
+ def _reader(asset: str, *args: Any, **kwargs: Any) -> PointData:
866
+ idx = asset_indexes.get(asset) or indexes
753
867
 
754
868
  asset_info = self._get_asset_info(asset)
755
- url = asset_info["url"]
869
+ reader, options = self._get_reader(asset_info)
870
+
756
871
  with self.ctx(**asset_info.get("env", {})):
757
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
872
+ with reader(
873
+ asset_info["url"],
874
+ tms=self.tms,
875
+ **{**self.reader_options, **options},
876
+ ) as src:
758
877
  data = src.point(*args, indexes=idx, **kwargs)
759
878
 
760
879
  metadata = data.metadata or {}
@@ -782,9 +901,9 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
782
901
  def feature(
783
902
  self,
784
903
  shape: Dict,
785
- assets: Union[Sequence[str], str] = None,
904
+ assets: Optional[Union[Sequence[str], str]] = None,
786
905
  expression: Optional[str] = None,
787
- asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset
906
+ asset_indexes: Optional[Dict[str, Indexes]] = None,
788
907
  asset_as_band: bool = False,
789
908
  **kwargs: Any,
790
909
  ) -> ImageData:
@@ -801,9 +920,7 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
801
920
  rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info.
802
921
 
803
922
  """
804
- if isinstance(assets, str):
805
- assets = (assets,)
806
-
923
+ assets = cast_to_sequence(assets)
807
924
  if assets and expression:
808
925
  warnings.warn(
809
926
  "Both expression and assets passed; expression will overwrite assets parameter.",
@@ -813,9 +930,16 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
813
930
  if expression:
814
931
  assets = self.parse_expression(expression, asset_as_band=asset_as_band)
815
932
 
933
+ if not assets and self.default_assets:
934
+ warnings.warn(
935
+ f"No assets/expression passed, defaults to {self.default_assets}",
936
+ UserWarning,
937
+ )
938
+ assets = self.default_assets
939
+
816
940
  if not assets:
817
941
  raise MissingAssets(
818
- "assets must be passed either via `expression` or `assets` options."
942
+ "assets must be passed via `expression` or `assets` options, or via class-level `default_assets`."
819
943
  )
820
944
 
821
945
  asset_indexes = asset_indexes or {}
@@ -824,12 +948,17 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta):
824
948
  indexes = kwargs.pop("indexes", None)
825
949
 
826
950
  def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData:
827
- idx = asset_indexes.get(asset) or indexes # type: ignore
951
+ idx = asset_indexes.get(asset) or indexes
828
952
 
829
953
  asset_info = self._get_asset_info(asset)
830
- url = asset_info["url"]
954
+ reader, options = self._get_reader(asset_info)
955
+
831
956
  with self.ctx(**asset_info.get("env", {})):
832
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
957
+ with reader(
958
+ asset_info["url"],
959
+ tms=self.tms,
960
+ **{**self.reader_options, **options},
961
+ ) as src:
833
962
  data = src.feature(*args, indexes=idx, **kwargs)
834
963
 
835
964
  self._update_statistics(
@@ -886,6 +1015,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
886
1015
  reader_options: Dict = attr.ib(factory=dict)
887
1016
 
888
1017
  bands: Sequence[str] = attr.ib(init=False)
1018
+ default_bands: Optional[Sequence[str]] = attr.ib(init=False, default=None)
889
1019
 
890
1020
  def __enter__(self):
891
1021
  """Support using with Context Managers."""
@@ -913,7 +1043,11 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
913
1043
 
914
1044
  return bands
915
1045
 
916
- def info(self, bands: Union[Sequence[str], str] = None, *args, **kwargs: Any) -> Info:
1046
+ def info(
1047
+ self,
1048
+ bands: Optional[Union[Sequence[str], str]] = None,
1049
+ **kwargs: Any,
1050
+ ) -> Info:
917
1051
  """Return metadata from multiple bands.
918
1052
 
919
1053
  Args:
@@ -929,22 +1063,22 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
929
1063
  UserWarning,
930
1064
  )
931
1065
 
932
- bands = bands or self.bands
933
-
934
- if isinstance(bands, str):
935
- bands = (bands,)
1066
+ bands = cast_to_sequence(bands or self.bands)
936
1067
 
937
1068
  def _reader(band: str, **kwargs: Any) -> Info:
938
1069
  url = self._get_band_url(band)
939
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
1070
+ with self.reader(
1071
+ url,
1072
+ tms=self.tms,
1073
+ **self.reader_options,
1074
+ ) as src:
940
1075
  return src.info()
941
1076
 
942
- bands_metadata = multi_values(bands, _reader, *args, **kwargs)
1077
+ bands_metadata = multi_values(bands, _reader, **kwargs)
943
1078
 
944
1079
  meta = {
945
- "bounds": self.geographic_bounds,
946
- "minzoom": self.minzoom,
947
- "maxzoom": self.maxzoom,
1080
+ "bounds": self.bounds,
1081
+ "crs": CRS_to_uri(self.crs) or self.crs.to_wkt(),
948
1082
  }
949
1083
 
950
1084
  # We only keep the value for the first band.
@@ -965,7 +1099,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
965
1099
 
966
1100
  def statistics(
967
1101
  self,
968
- bands: Union[Sequence[str], str] = None,
1102
+ bands: Optional[Union[Sequence[str], str]] = None,
969
1103
  expression: Optional[str] = None,
970
1104
  categorical: bool = False,
971
1105
  categories: Optional[List[float]] = None,
@@ -996,7 +1130,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
996
1130
  "No `bands` option passed, will fetch statistics for all available bands.",
997
1131
  UserWarning,
998
1132
  )
999
- bands = bands or self.bands
1133
+ bands = cast_to_sequence(bands or self.bands)
1000
1134
 
1001
1135
  data = self.preview(
1002
1136
  bands=bands,
@@ -1016,7 +1150,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1016
1150
  tile_x: int,
1017
1151
  tile_y: int,
1018
1152
  tile_z: int,
1019
- bands: Union[Sequence[str], str] = None,
1153
+ bands: Optional[Union[Sequence[str], str]] = None,
1020
1154
  expression: Optional[str] = None,
1021
1155
  **kwargs: Any,
1022
1156
  ) -> ImageData:
@@ -1039,9 +1173,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1039
1173
  f"Tile(x={tile_x}, y={tile_y}, z={tile_z}) is outside bounds"
1040
1174
  )
1041
1175
 
1042
- if isinstance(bands, str):
1043
- bands = (bands,)
1044
-
1176
+ bands = cast_to_sequence(bands)
1045
1177
  if bands and expression:
1046
1178
  warnings.warn(
1047
1179
  "Both expression and bands passed; expression will overwrite bands parameter.",
@@ -1051,6 +1183,13 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1051
1183
  if expression:
1052
1184
  bands = self.parse_expression(expression)
1053
1185
 
1186
+ if not bands and self.default_bands:
1187
+ warnings.warn(
1188
+ f"No bands/expression passed, defaults to {self.default_bands}",
1189
+ UserWarning,
1190
+ )
1191
+ bands = self.default_bands
1192
+
1054
1193
  if not bands:
1055
1194
  raise MissingBands(
1056
1195
  "bands must be passed either via `expression` or `bands` options."
@@ -1058,11 +1197,19 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1058
1197
 
1059
1198
  def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData:
1060
1199
  url = self._get_band_url(band)
1061
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
1200
+ with self.reader(
1201
+ url,
1202
+ tms=self.tms,
1203
+ **self.reader_options,
1204
+ ) as src:
1062
1205
  data = src.tile(*args, **kwargs)
1206
+
1063
1207
  if data.metadata:
1064
1208
  data.metadata = {band: data.metadata}
1065
- data.band_names = [band] # use `band` as name instead of band index
1209
+
1210
+ # use `band` as name instead of band index
1211
+ data.band_names = [band]
1212
+
1066
1213
  return data
1067
1214
 
1068
1215
  img = multi_arrays(bands, _reader, tile_x, tile_y, tile_z, **kwargs)
@@ -1075,7 +1222,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1075
1222
  def part(
1076
1223
  self,
1077
1224
  bbox: BBox,
1078
- bands: Union[Sequence[str], str] = None,
1225
+ bands: Optional[Union[Sequence[str], str]] = None,
1079
1226
  expression: Optional[str] = None,
1080
1227
  **kwargs: Any,
1081
1228
  ) -> ImageData:
@@ -1091,9 +1238,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1091
1238
  rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info.
1092
1239
 
1093
1240
  """
1094
- if isinstance(bands, str):
1095
- bands = (bands,)
1096
-
1241
+ bands = cast_to_sequence(bands)
1097
1242
  if bands and expression:
1098
1243
  warnings.warn(
1099
1244
  "Both expression and bands passed; expression will overwrite bands parameter.",
@@ -1103,6 +1248,13 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1103
1248
  if expression:
1104
1249
  bands = self.parse_expression(expression)
1105
1250
 
1251
+ if not bands and self.default_bands:
1252
+ warnings.warn(
1253
+ f"No bands/expression passed, defaults to {self.default_bands}",
1254
+ UserWarning,
1255
+ )
1256
+ bands = self.default_bands
1257
+
1106
1258
  if not bands:
1107
1259
  raise MissingBands(
1108
1260
  "bands must be passed either via `expression` or `bands` options."
@@ -1110,11 +1262,19 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1110
1262
 
1111
1263
  def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData:
1112
1264
  url = self._get_band_url(band)
1113
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
1265
+ with self.reader(
1266
+ url,
1267
+ tms=self.tms,
1268
+ **self.reader_options,
1269
+ ) as src:
1114
1270
  data = src.part(*args, **kwargs)
1271
+
1115
1272
  if data.metadata:
1116
1273
  data.metadata = {band: data.metadata}
1117
- data.band_names = [band] # use `band` as name instead of band index
1274
+
1275
+ # use `band` as name instead of band index
1276
+ data.band_names = [band]
1277
+
1118
1278
  return data
1119
1279
 
1120
1280
  img = multi_arrays(bands, _reader, bbox, **kwargs)
@@ -1126,7 +1286,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1126
1286
 
1127
1287
  def preview(
1128
1288
  self,
1129
- bands: Union[Sequence[str], str] = None,
1289
+ bands: Optional[Union[Sequence[str], str]] = None,
1130
1290
  expression: Optional[str] = None,
1131
1291
  **kwargs: Any,
1132
1292
  ) -> ImageData:
@@ -1141,9 +1301,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1141
1301
  rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info.
1142
1302
 
1143
1303
  """
1144
- if isinstance(bands, str):
1145
- bands = (bands,)
1146
-
1304
+ bands = cast_to_sequence(bands)
1147
1305
  if bands and expression:
1148
1306
  warnings.warn(
1149
1307
  "Both expression and bands passed; expression will overwrite bands parameter.",
@@ -1153,6 +1311,13 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1153
1311
  if expression:
1154
1312
  bands = self.parse_expression(expression)
1155
1313
 
1314
+ if not bands and self.default_bands:
1315
+ warnings.warn(
1316
+ f"No bands/expression passed, defaults to {self.default_bands}",
1317
+ UserWarning,
1318
+ )
1319
+ bands = self.default_bands
1320
+
1156
1321
  if not bands:
1157
1322
  raise MissingBands(
1158
1323
  "bands must be passed either via `expression` or `bands` options."
@@ -1160,11 +1325,19 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1160
1325
 
1161
1326
  def _reader(band: str, **kwargs: Any) -> ImageData:
1162
1327
  url = self._get_band_url(band)
1163
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
1328
+ with self.reader(
1329
+ url,
1330
+ tms=self.tms,
1331
+ **self.reader_options,
1332
+ ) as src:
1164
1333
  data = src.preview(**kwargs)
1334
+
1165
1335
  if data.metadata:
1166
1336
  data.metadata = {band: data.metadata}
1167
- data.band_names = [band] # use `band` as name instead of band index
1337
+
1338
+ # use `band` as name instead of band index
1339
+ data.band_names = [band]
1340
+
1168
1341
  return data
1169
1342
 
1170
1343
  img = multi_arrays(bands, _reader, **kwargs)
@@ -1178,7 +1351,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1178
1351
  self,
1179
1352
  lon: float,
1180
1353
  lat: float,
1181
- bands: Union[Sequence[str], str] = None,
1354
+ bands: Optional[Union[Sequence[str], str]] = None,
1182
1355
  expression: Optional[str] = None,
1183
1356
  **kwargs: Any,
1184
1357
  ) -> PointData:
@@ -1195,9 +1368,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1195
1368
  PointData
1196
1369
 
1197
1370
  """
1198
- if isinstance(bands, str):
1199
- bands = (bands,)
1200
-
1371
+ bands = cast_to_sequence(bands)
1201
1372
  if bands and expression:
1202
1373
  warnings.warn(
1203
1374
  "Both expression and bands passed; expression will overwrite bands parameter.",
@@ -1207,18 +1378,33 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1207
1378
  if expression:
1208
1379
  bands = self.parse_expression(expression)
1209
1380
 
1381
+ if not bands and self.default_bands:
1382
+ warnings.warn(
1383
+ f"No bands/expression passed, defaults to {self.default_bands}",
1384
+ UserWarning,
1385
+ )
1386
+ bands = self.default_bands
1387
+
1210
1388
  if not bands:
1211
1389
  raise MissingBands(
1212
1390
  "bands must be passed either via `expression` or `bands` options."
1213
1391
  )
1214
1392
 
1215
- def _reader(band: str, *args, **kwargs: Any) -> PointData:
1393
+ def _reader(band: str, *args: Any, **kwargs: Any) -> PointData:
1216
1394
  url = self._get_band_url(band)
1217
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
1395
+ with self.reader(
1396
+ url,
1397
+ tms=self.tms,
1398
+ **self.reader_options,
1399
+ ) as src:
1218
1400
  data = src.point(*args, **kwargs)
1401
+
1219
1402
  if data.metadata:
1220
1403
  data.metadata = {band: data.metadata}
1221
- data.band_names = [band] # use `band` as name instead of band index
1404
+
1405
+ # use `band` as name instead of band index
1406
+ data.band_names = [band]
1407
+
1222
1408
  return data
1223
1409
 
1224
1410
  data = multi_points(bands, _reader, lon, lat, **kwargs)
@@ -1230,7 +1416,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1230
1416
  def feature(
1231
1417
  self,
1232
1418
  shape: Dict,
1233
- bands: Union[Sequence[str], str] = None,
1419
+ bands: Optional[Union[Sequence[str], str]] = None,
1234
1420
  expression: Optional[str] = None,
1235
1421
  **kwargs: Any,
1236
1422
  ) -> ImageData:
@@ -1246,9 +1432,7 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1246
1432
  rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info.
1247
1433
 
1248
1434
  """
1249
- if isinstance(bands, str):
1250
- bands = (bands,)
1251
-
1435
+ bands = cast_to_sequence(bands)
1252
1436
  if bands and expression:
1253
1437
  warnings.warn(
1254
1438
  "Both expression and bands passed; expression will overwrite bands parameter.",
@@ -1258,6 +1442,13 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1258
1442
  if expression:
1259
1443
  bands = self.parse_expression(expression)
1260
1444
 
1445
+ if not bands and self.default_bands:
1446
+ warnings.warn(
1447
+ f"No bands/expression passed, defaults to {self.default_bands}",
1448
+ UserWarning,
1449
+ )
1450
+ bands = self.default_bands
1451
+
1261
1452
  if not bands:
1262
1453
  raise MissingBands(
1263
1454
  "bands must be passed either via `expression` or `bands` options."
@@ -1265,11 +1456,19 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta):
1265
1456
 
1266
1457
  def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData:
1267
1458
  url = self._get_band_url(band)
1268
- with self.reader(url, tms=self.tms, **self.reader_options) as src: # type: ignore
1459
+ with self.reader(
1460
+ url,
1461
+ tms=self.tms,
1462
+ **self.reader_options,
1463
+ ) as src:
1269
1464
  data = src.feature(*args, **kwargs)
1465
+
1270
1466
  if data.metadata:
1271
1467
  data.metadata = {band: data.metadata}
1272
- data.band_names = [band] # use `band` as name instead of band index
1468
+
1469
+ # use `band` as name instead of band index
1470
+ data.band_names = [band]
1471
+
1273
1472
  return data
1274
1473
 
1275
1474
  img = multi_arrays(bands, _reader, shape, **kwargs)