diff --git a/docs/src/migrations/v7_migration.md b/docs/src/migrations/v7_migration.md new file mode 100644 index 00000000..a0323a63 --- /dev/null +++ b/docs/src/migrations/v7_migration.md @@ -0,0 +1,80 @@ + +`rio-tiler` version 7.0 introduced [many breaking changes](../release-notes.md). This +document aims to help with migrating your code to use `rio-tiler` 7.0. + +## `SpatialMixin` class and `TMS` + +The `SpatialMixin` class, used in `BaseReader` class had numerous breaking changes: + +- removed `tms` and `geographic_crs` attributes + +- changed `geographic_bounds(self)` from `property` to `method` -> `geographic_bounds(self, geographic_crs: CRS = WGS84_CRS)` + +- removed `_dst_geom_in_tms_crs` method + +- removed `_minzoom` and `_maxzoom` property + +The main change is the removal of the `tms` attribute, which means the `BaseReader` do not have `tms` information, thus the `.tile()` now needs a tms argument: + +```python +# before +import morecantile +from rio_tiler.io import Reader + +with Reader("cog.tif", tms=morecantile.tms.get("WebMercatorQuad")) as src: + _ = src.tile(0, 0, 0) + +# now +with Reader("cog.tif") as src: + _ = src.tile(0, 0, 0, tms=morecantile.tms.get("WebMercatorQuad")) +``` + +same for the `tile_exists` method + +```python +# before +import morecantile +from rio_tiler.io import Reader + +with Reader("cog.tif", tms=morecantile.tms.get("WebMercatorQuad")) as src: + _ = src.tile_exists(0, 0, 0) + +# now +with Reader("cog.tif") as src: + _ = src.tile_exists(0, 0, 0, tms=morecantile.tms.get("WebMercatorQuad")) +``` + +By removing the `tms` attribute, we also removed the `min/max zoom` properties. We've added `.get_zooms(tms: TileMatrixSet)` method to the `SpatialMixin` class to allow users to retrieve the zoom information: + +```python +# before +import morecantile +from rio_tiler.io import Reader + +with Reader("cog.tif", tms=morecantile.tms.get("WebMercatorQuad")) as src: + minzoom, maxzoom = src.minzoom, src.maxzoom + +# now +with Reader("cog.tif") as src: + minzoom, maxzoom = src.get_zooms(morecantile.tms.get("WebMercatorQuad")) +``` + +Same for the `geographic_bounds`, which now needs to be retrieved using the `geographic_bounds(tms: TileMatrixSet)` method (instead of the `.geographic_bounds` property), because we've removed the `geographic_crs` attribute. + +```python +# before +from rasterio.crs import CRS +from rio_tiler.io import Reader + +with Reader("cog.tif", geographic_crs=CRS.from_epsg(4326)) as src: + _ = src.geographic_bounds + +# now +with Reader("cog.tif") as src: + _ = src.geographic_bounds(CRS.from_epsg(4326)) +``` + + +## `Info()` and Models + +- `rio_tiler.models.SpatialInfo` was removed diff --git a/rio_tiler/io/base.py b/rio_tiler/io/base.py index 74390120..7f5bb5bd 100644 --- a/rio_tiler/io/base.py +++ b/rio_tiler/io/base.py @@ -4,7 +4,6 @@ import contextlib import re import warnings -from functools import cached_property from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union import attr @@ -39,8 +38,6 @@ class SpatialMixin: """ - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - bounds: BBox = attr.ib(init=False) crs: CRS = attr.ib(init=False) @@ -48,12 +45,9 @@ class SpatialMixin: height: Optional[int] = attr.ib(default=None, init=False) width: Optional[int] = attr.ib(default=None, init=False) - geographic_crs: CRS = attr.ib(init=False, default=WGS84_CRS) - - @cached_property - def geographic_bounds(self) -> BBox: + def geographic_bounds(self, crs: CRS = WGS84_CRS) -> BBox: """Return dataset bounds in geographic_crs.""" - if self.crs == self.geographic_crs: + if self.crs == crs: if self.bounds[1] > self.bounds[3]: warnings.warn( "BoundingBox of the dataset is inverted (minLat > maxLat).", @@ -71,7 +65,7 @@ def geographic_bounds(self) -> BBox: try: bounds = transform_bounds( self.crs, - self.geographic_crs, + crs, *self.bounds, densify_pts=21, ) @@ -91,98 +85,77 @@ def geographic_bounds(self) -> BBox: return bounds - @cached_property - def _dst_geom_in_tms_crs(self): - """Return dataset geom info in TMS projection.""" - tms_crs = self.tms.rasterio_crs - if self.crs != tms_crs: - dst_affine, w, h = calculate_default_transform( - self.crs, - tms_crs, - self.width, - self.height, - *self.bounds, - ) - else: - dst_affine = list(self.transform) - w = self.width - h = self.height - - return dst_affine, w, h - - @cached_property - def _minzoom(self) -> int: - """Calculate dataset minimum zoom level.""" - # We assume the TMS tilesize to be constant over all matrices - # ref: https://github.com/OSGeo/gdal/blob/dc38aa64d779ecc45e3cd15b1817b83216cf96b8/gdal/frmts/gtiff/cogdriver.cpp#L274 - tilesize = self.tms.tileMatrices[0].tileWidth - + def get_zooms(self, tms: TileMatrixSet) -> Tuple[int, int]: + """Get Min/Max zooms for a dataset.""" + minzoom, maxzoom = tms.minzoom, tms.maxzoom if all([self.transform, self.height, self.width]): + tms_crs = tms.rasterio_crs + dataset_crs = self.crs + try: - dst_affine, w, h = self._dst_geom_in_tms_crs + dst_affine = list(self.transform) + w = self.width + h = self.height + if tms_crs != dataset_crs: + dst_affine, w, h = calculate_default_transform( + self.crs, + tms_crs, + self.width, + self.height, + *self.bounds, + ) + # --- MinZoom (based on lowest virtual overview resolution) --- # The minzoom is defined by the resolution of the maximum theoretical overview level # We assume `tilesize`` is the smallest overview size + tilesize = tms.tileMatrices[0].tileWidth overview_level = get_maximum_overview_level(w, h, minsize=tilesize) # Get the resolution of the overview resolution = max(abs(dst_affine[0]), abs(dst_affine[4])) ovr_resolution = resolution * (2**overview_level) - # Find what TMS matrix match the overview resolution - return self.tms.zoom_for_res(ovr_resolution) + minzoom = tms.zoom_for_res(ovr_resolution) - except: # noqa - # if we can't get max zoom from the dataset we default to TMS maxzoom - warnings.warn( - "Cannot determine minzoom based on dataset information, will default to TMS minzoom.", - UserWarning, - ) - - return self.tms.minzoom - - @cached_property - def _maxzoom(self) -> int: - """Calculate dataset maximum zoom level.""" - if all([self.transform, self.height, self.width]): - try: - dst_affine, _, _ = self._dst_geom_in_tms_crs - - # The maxzoom is defined by finding the minimum difference between - # the raster resolution and the zoom level resolution + # --- MaxZoom (based on raw resolution) --- resolution = max(abs(dst_affine[0]), abs(dst_affine[4])) - return self.tms.zoom_for_res(resolution) + maxzoom = tms.zoom_for_res(resolution) except: # noqa - # if we can't get min/max zoom from the dataset we default to TMS maxzoom + # if we can't get max zoom from the dataset we default to TMS maxzoom warnings.warn( - "Cannot determine maxzoom based on dataset information, will default to TMS maxzoom.", + "Cannot determine minzoom/maxzoom based on dataset information, will default to TMS minzoom/maxzoom.", UserWarning, ) - return self.tms.maxzoom + return minzoom, maxzoom - def tile_exists(self, tile_x: int, tile_y: int, tile_z: int) -> bool: + def tile_exists( + self, tile_x: int, tile_y: int, tile_z: int, tms: TileMatrixSet + ) -> bool: """Check if a tile intersects the dataset bounds. Args: tile_x (int): Tile's horizontal index. tile_y (int): Tile's vertical index. tile_z (int): Tile's zoom level index. + tms (TileMatrixSet): TileMatrixSet. Returns: bool: True if the tile intersects the dataset bounds. """ # bounds in TileMatrixSet's CRS - tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) + tile_bounds = tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) - if not self.tms.rasterio_crs == self.crs: + tms_crs = tms.rasterio_crs + dataset_crs = self.crs + if not tms_crs == dataset_crs: # Transform the bounds to the dataset's CRS try: tile_bounds = transform_bounds( - self.tms.rasterio_crs, - self.crs, + tms_crs, + dataset_crs, *tile_bounds, densify_pts=21, ) @@ -191,8 +164,8 @@ def tile_exists(self, tile_x: int, tile_y: int, tile_z: int) -> bool: # but if retried it will then pass. # Note: It might return `+/-inf` values tile_bounds = transform_bounds( - self.tms.rasterio_crs, - self.crs, + tms_crs, + dataset_crs, *tile_bounds, densify_pts=21, ) @@ -223,7 +196,6 @@ class BaseReader(SpatialMixin, metaclass=abc.ABCMeta): """ input: Any = attr.ib() - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) def __enter__(self): """Support using with Context Managers.""" @@ -335,10 +307,6 @@ class MultiBaseReader(SpatialMixin, metaclass=abc.ABCMeta): """ input: Any = attr.ib() - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - - minzoom: int = attr.ib(default=None) - maxzoom: int = attr.ib(default=None) reader: Type[BaseReader] = attr.ib(init=False) reader_options: Dict = attr.ib(factory=dict) @@ -431,9 +399,7 @@ def _reader(asset: str, **kwargs: Any) -> Dict: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: return src.info() @@ -474,9 +440,7 @@ def _reader(asset: str, *args: Any, **kwargs: Any) -> Dict: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: return src.statistics( *args, @@ -544,6 +508,7 @@ def tile( tile_x: int, tile_y: int, tile_z: int, + tms: TileMatrixSet = WEB_MERCATOR_TMS, assets: Optional[Union[Sequence[str], str]] = None, expression: Optional[str] = None, asset_indexes: Optional[Dict[str, Indexes]] = None, @@ -565,7 +530,7 @@ def tile( rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info. """ - if not self.tile_exists(tile_x, tile_y, tile_z): + if not self.tile_exists(tile_x, tile_y, tile_z, tms): raise TileOutsideBounds( f"Tile(x={tile_x}, y={tile_y}, z={tile_z}) is outside bounds" ) @@ -605,9 +570,7 @@ def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: data = src.tile(*args, indexes=idx, **kwargs) @@ -633,7 +596,7 @@ def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: return data - img = multi_arrays(assets, _reader, tile_x, tile_y, tile_z, **kwargs) + img = multi_arrays(assets, _reader, tile_x, tile_y, tile_z, tms=tms, **kwargs) if expression: return img.apply_expression(expression) @@ -696,9 +659,7 @@ def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: data = src.part(*args, indexes=idx, **kwargs) @@ -785,9 +746,7 @@ def _reader(asset: str, **kwargs: Any) -> ImageData: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: data = src.preview(indexes=idx, **kwargs) @@ -878,9 +837,7 @@ def _reader(asset: str, *args: Any, **kwargs: Any) -> PointData: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: data = src.point(*args, indexes=idx, **kwargs) @@ -963,9 +920,7 @@ def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: with self.ctx(**asset_info.get("env", {})): with reader( - asset_info["url"], - tms=self.tms, - **{**self.reader_options, **options}, + asset_info["url"], **{**self.reader_options, **options} ) as src: data = src.feature(*args, indexes=idx, **kwargs) @@ -1014,10 +969,6 @@ class MultiBandReader(SpatialMixin, metaclass=abc.ABCMeta): """ input: Any = attr.ib() - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - - minzoom: int = attr.ib(default=None) - maxzoom: int = attr.ib(default=None) reader: Type[BaseReader] = attr.ib(init=False) reader_options: Dict = attr.ib(factory=dict) @@ -1075,19 +1026,16 @@ def info( def _reader(band: str, **kwargs: Any) -> Info: url = self._get_band_url(band) - with self.reader( - url, - tms=self.tms, - **self.reader_options, - ) as src: + with self.reader(url, **self.reader_options) as src: return src.info() bands_metadata = multi_values(bands, _reader, **kwargs) meta = { - "bounds": self.geographic_bounds, - "minzoom": self.minzoom, - "maxzoom": self.maxzoom, + "bounds": self.bounds, + "crs": f"EPSG:{self.crs.to_epsg()}" + if self.crs.to_epsg() + else self.crs.to_wkt(), } # We only keep the value for the first band. @@ -1159,6 +1107,7 @@ def tile( tile_x: int, tile_y: int, tile_z: int, + tms: TileMatrixSet = WEB_MERCATOR_TMS, bands: Optional[Union[Sequence[str], str]] = None, expression: Optional[str] = None, **kwargs: Any, @@ -1177,7 +1126,7 @@ def tile( rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info. """ - if not self.tile_exists(tile_x, tile_y, tile_z): + if not self.tile_exists(tile_x, tile_y, tile_z, tms): raise TileOutsideBounds( f"Tile(x={tile_x}, y={tile_y}, z={tile_z}) is outside bounds" ) @@ -1206,11 +1155,7 @@ def tile( def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData: url = self._get_band_url(band) - with self.reader( - url, - tms=self.tms, - **self.reader_options, - ) as src: + with self.reader(url, **self.reader_options) as src: data = src.tile(*args, **kwargs) if data.metadata: @@ -1221,7 +1166,7 @@ def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData: return data - img = multi_arrays(bands, _reader, tile_x, tile_y, tile_z, **kwargs) + img = multi_arrays(bands, _reader, tile_x, tile_y, tile_z, tms=tms, **kwargs) if expression: return img.apply_expression(expression) @@ -1271,11 +1216,7 @@ def part( def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData: url = self._get_band_url(band) - with self.reader( - url, - tms=self.tms, - **self.reader_options, - ) as src: + with self.reader(url, **self.reader_options) as src: data = src.part(*args, **kwargs) if data.metadata: @@ -1334,11 +1275,7 @@ def preview( def _reader(band: str, **kwargs: Any) -> ImageData: url = self._get_band_url(band) - with self.reader( - url, - tms=self.tms, - **self.reader_options, - ) as src: + with self.reader(url, **self.reader_options) as src: data = src.preview(**kwargs) if data.metadata: @@ -1401,11 +1338,7 @@ def point( def _reader(band: str, *args: Any, **kwargs: Any) -> PointData: url = self._get_band_url(band) - with self.reader( - url, - tms=self.tms, - **self.reader_options, - ) as src: + with self.reader(url, **self.reader_options) as src: data = src.point(*args, **kwargs) if data.metadata: @@ -1465,11 +1398,7 @@ def feature( def _reader(band: str, *args: Any, **kwargs: Any) -> ImageData: url = self._get_band_url(band) - with self.reader( - url, - tms=self.tms, - **self.reader_options, - ) as src: + with self.reader(url, **self.reader_options) as src: data = src.feature(*args, **kwargs) if data.metadata: diff --git a/rio_tiler/io/rasterio.py b/rio_tiler/io/rasterio.py index 60066841..1b9023bc 100644 --- a/rio_tiler/io/rasterio.py +++ b/rio_tiler/io/rasterio.py @@ -2,7 +2,7 @@ import contextlib import warnings -from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union import attr import numpy @@ -34,7 +34,7 @@ from rio_tiler.expression import parse_expression from rio_tiler.io.base import BaseReader from rio_tiler.models import BandStatistics, ImageData, Info, PointData -from rio_tiler.types import BBox, Indexes, NumType, RIOResampling +from rio_tiler.types import BBox, Indexes, NoData, NumType, RIOResampling from rio_tiler.utils import _validate_shape_input, has_alpha_band, has_mask_band @@ -45,8 +45,6 @@ class Reader(BaseReader): Attributes: input (str): dataset path. dataset (rasterio.io.DatasetReader or rasterio.io.DatasetWriter or rasterio.vrt.WarpedVRT, optional): Rasterio dataset. - tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`. - geographic_crs (rasterio.crs.CRS, optional): CRS to use as geographic coordinate system. Defaults to WGS84. colormap (dict, optional): Overwrite internal colormap. options (dict, optional): Options to forward to low-level reader methods. @@ -74,9 +72,6 @@ class Reader(BaseReader): default=None ) - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - geographic_crs: CRS = attr.ib(default=WGS84_CRS) - colormap: Dict = attr.ib(default=None) options: reader.Options = attr.ib() @@ -142,16 +137,6 @@ def __exit__(self, exc_type, exc_value, traceback): """Support using with Context Managers.""" self.close() - @property - def minzoom(self): - """Return dataset minzoom.""" - return self._minzoom - - @property - def maxzoom(self): - """Return dataset maxzoom.""" - return self._maxzoom - def _get_colormap(self): """Retrieve the internal colormap.""" try: @@ -176,10 +161,13 @@ def _get_descr(ix): else: nodata_type = "None" + crs_string = ( + f"EPSG:{self.crs.to_epsg()}" if self.crs.to_epsg() else self.crs.to_wkt() + ) + meta = { - "bounds": self.geographic_bounds, - "minzoom": self.minzoom, - "maxzoom": self.maxzoom, + "bounds": self.bounds, + "crs": crs_string, "band_metadata": [ (f"b{ix}", self.dataset.tags(ix)) for ix in self.dataset.indexes ], @@ -252,6 +240,7 @@ def tile( tile_y: int, tile_z: int, tilesize: int = 256, + tms: TileMatrixSet = WEB_MERCATOR_TMS, indexes: Optional[Indexes] = None, expression: Optional[str] = None, buffer: Optional[float] = None, @@ -273,18 +262,17 @@ def tile( rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info. """ - if not self.tile_exists(tile_x, tile_y, tile_z): + if not self.tile_exists(tile_x, tile_y, tile_z, tms): raise TileOutsideBounds( f"Tile(x={tile_x}, y={tile_y}, z={tile_z}) is outside bounds" ) - tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) - dst_crs = self.tms.rasterio_crs + tile_bounds = tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) return self.part( tile_bounds, - dst_crs=dst_crs, - bounds_crs=dst_crs, + dst_crs=tms.rasterio_crs, + bounds_crs=tms.rasterio_crs, height=tilesize, width=tilesize, max_size=None, @@ -594,10 +582,7 @@ class ImageReader(Reader): """Non Geo Image Reader""" tms: TileMatrixSet = attr.ib(init=False) - crs: CRS = attr.ib(init=False, default=None) - geographic_crs: CRS = attr.ib(init=False, default=None) - transform: Affine = attr.ib(init=False) def __attrs_post_init__(self): @@ -622,15 +607,9 @@ def __attrs_post_init__(self): NoOverviewWarning, ) - @property - def minzoom(self): - """Return dataset minzoom.""" - return self.tms.minzoom - - @property - def maxzoom(self): - """Return dataset maxzoom.""" - return self.tms.maxzoom + def get_zooms(self) -> Tuple[int, int]: # type: ignore + """Return min/max zooms.""" + return self.tms.minzoom, self.tms.maxzoom def tile( # type: ignore self, @@ -642,6 +621,7 @@ def tile( # type: ignore expression: Optional[str] = None, force_binary_mask: bool = True, resampling_method: RIOResampling = "nearest", + nodata: Optional[NoData] = None, unscale: bool = False, post_process: Optional[ Callable[[numpy.ma.MaskedArray], numpy.ma.MaskedArray] @@ -665,7 +645,7 @@ def tile( # type: ignore rio_tiler.models.ImageData: ImageData instance with data, mask and tile spatial info. """ - if not self.tile_exists(tile_x, tile_y, tile_z): + if not self.tile_exists(tile_x, tile_y, tile_z, self.tms): raise TileOutsideBounds( f"Tile {tile_z}/{tile_x}/{tile_y} is outside {self.input} bounds" ) @@ -680,6 +660,7 @@ def tile( # type: ignore indexes=indexes, expression=expression, force_binary_mask=force_binary_mask, + nodata=nodata, resampling_method=resampling_method, unscale=unscale, post_process=post_process, @@ -694,6 +675,7 @@ def part( # type: ignore height: Optional[int] = None, width: Optional[int] = None, force_binary_mask: bool = True, + nodata: Optional[NoData] = None, resampling_method: RIOResampling = "nearest", unscale: bool = False, post_process: Optional[ @@ -736,6 +718,7 @@ def part( # type: ignore height=height, indexes=indexes, force_binary_mask=force_binary_mask, + nodata=nodata, resampling_method=resampling_method, unscale=unscale, post_process=post_process, @@ -753,6 +736,7 @@ def point( # type: ignore y: float, indexes: Optional[Indexes] = None, expression: Optional[str] = None, + nodata: Optional[NoData] = None, unscale: bool = False, post_process: Optional[ Callable[[numpy.ma.MaskedArray], numpy.ma.MaskedArray] @@ -778,6 +762,7 @@ def point( # type: ignore img = self.read( indexes=indexes, expression=expression, + nodata=nodata, unscale=unscale, post_process=post_process, window=Window(col_off=x, row_off=y, width=1, height=1), @@ -800,6 +785,7 @@ def feature( # type: ignore height: Optional[int] = None, width: Optional[int] = None, force_binary_mask: bool = True, + nodata: Optional[NoData] = None, resampling_method: RIOResampling = "nearest", unscale: bool = False, post_process: Optional[ @@ -819,6 +805,7 @@ def feature( # type: ignore height=height, width=width, force_binary_mask=force_binary_mask, + nodata=nodata, resampling_method=resampling_method, unscale=unscale, post_process=post_process, diff --git a/rio_tiler/io/stac.py b/rio_tiler/io/stac.py index 0a05111e..734e7670 100644 --- a/rio_tiler/io/stac.py +++ b/rio_tiler/io/stac.py @@ -12,11 +12,9 @@ import rasterio from cachetools import LRUCache, cached from cachetools.keys import hashkey -from morecantile import TileMatrixSet -from rasterio.crs import CRS from rasterio.transform import array_bounds -from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS +from rio_tiler.constants import WGS84_CRS from rio_tiler.errors import InvalidAssetName, MissingAssets from rio_tiler.io.base import BaseReader, MultiBaseReader from rio_tiler.io.rasterio import Reader @@ -195,10 +193,6 @@ class STACReader(MultiBaseReader): Attributes: input (str): STAC Item path, URL or S3 URL. item (dict or pystac.Item, STAC): Stac Item. - tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`. - minzoom (int, optional): Set minzoom for the tiles. - maxzoom (int, optional): Set maxzoom for the tiles. - geographic_crs (rasterio.crs.CRS, optional): CRS to use as geographic coordinate system. Defaults to WGS84. include_assets (set of string, optional): Only Include specific assets. exclude_assets (set of string, optional): Exclude specific assets. include_asset_types (set of string, optional): Only include some assets base on their type. @@ -230,12 +224,6 @@ class STACReader(MultiBaseReader): input: str = attr.ib() item: pystac.Item = attr.ib(default=None, converter=_to_pystac_item) - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - minzoom: int = attr.ib(default=None) - maxzoom: int = attr.ib(default=None) - - geographic_crs: CRS = attr.ib(default=WGS84_CRS) - include_assets: Optional[Set[str]] = attr.ib(default=None) exclude_assets: Optional[Set[str]] = attr.ib(default=None) @@ -274,9 +262,6 @@ def __attrs_post_init__(self): self.bounds = array_bounds(self.height, self.width, self.transform) self.crs = rasterio.crs.CRS.from_string(self.item.ext.proj.crs_string) - self.minzoom = self.minzoom if self.minzoom is not None else self._minzoom - self.maxzoom = self.maxzoom if self.maxzoom is not None else self._maxzoom - self.assets = list( _get_assets( self.item, diff --git a/rio_tiler/io/xarray.py b/rio_tiler/io/xarray.py index 64af201e..2e74dd70 100644 --- a/rio_tiler/io/xarray.py +++ b/rio_tiler/io/xarray.py @@ -41,9 +41,7 @@ class XarrayReader(BaseReader): """Xarray Reader. Attributes: - dataset (xarray.DataArray): Xarray DataArray dataset. - tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`. - geographic_crs (rasterio.crs.CRS, optional): CRS to use as geographic coordinate system. Defaults to WGS84. + input (xarray.DataArray): Xarray DataArray dataset. Examples: >>> ds = xarray.open_dataset( @@ -61,9 +59,6 @@ class XarrayReader(BaseReader): input: xarray.DataArray = attr.ib() - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - geographic_crs: CRS = attr.ib(default=WGS84_CRS) - _dims: List = attr.ib(init=False, factory=list) def __attrs_post_init__(self): @@ -99,16 +94,6 @@ def __attrs_post_init__(self): if d not in [self.input.rio.x_dim, self.input.rio.y_dim] ] - @property - def minzoom(self): - """Return dataset minzoom.""" - return self._minzoom - - @property - def maxzoom(self): - """Return dataset maxzoom.""" - return self._maxzoom - @property def band_names(self) -> List[str]: """Return list of `band names` in DataArray.""" @@ -118,11 +103,13 @@ def info(self) -> Info: """Return xarray.DataArray info.""" bands = [str(band) for d in self._dims for band in self.input[d].values] metadata = [band.attrs for d in self._dims for band in self.input[d]] + crs_string = ( + f"EPSG:{self.crs.to_epsg()}" if self.crs.to_epsg() else self.crs.to_wkt() + ) meta = { - "bounds": self.geographic_bounds, - "minzoom": self.minzoom, - "maxzoom": self.maxzoom, + "bounds": self.bounds, + "crs": crs_string, "band_metadata": [(f"b{ix}", v) for ix, v in enumerate(metadata, 1)], "band_descriptions": [(f"b{ix}", v) for ix, v in enumerate(bands, 1)], "dtype": str(self.input.dtype), @@ -153,6 +140,7 @@ def tile( tile_y: int, tile_z: int, tilesize: int = 256, + tms: TileMatrixSet = WEB_MERCATOR_TMS, resampling_method: Optional[WarpResampling] = None, reproject_method: WarpResampling = "nearest", auto_expand: bool = True, @@ -181,7 +169,7 @@ def tile( ) reproject_method = resampling_method - if not self.tile_exists(tile_x, tile_y, tile_z): + if not self.tile_exists(tile_x, tile_y, tile_z, tms): raise TileOutsideBounds( f"Tile(x={tile_x}, y={tile_y}, z={tile_z}) is outside bounds" ) @@ -190,8 +178,8 @@ def tile( if nodata is not None: ds = ds.rio.write_nodata(nodata) - tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) - dst_crs = self.tms.rasterio_crs + tile_bounds = tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) + dst_crs = tms.rasterio_crs # Create source array by clipping the xarray dataset to extent of the tile. ds = ds.rio.clip_box( diff --git a/rio_tiler/models.py b/rio_tiler/models.py index c00d9c18..712a20ee 100644 --- a/rio_tiler/models.py +++ b/rio_tiler/models.py @@ -57,22 +57,11 @@ def __getitem__(self, item): return {**self.__dict__, **self.__pydantic_extra__}[item] -class Bounds(RioTilerBaseModel): - """Dataset Bounding box""" - - bounds: BoundingBox - - -class SpatialInfo(Bounds): - """Dataset SpatialInfo""" - - minzoom: int - maxzoom: int - - -class Info(SpatialInfo): +class Info(RioTilerBaseModel): """Dataset Info.""" + bounds: BoundingBox + crs: str band_metadata: List[Tuple[str, Dict]] band_descriptions: List[Tuple[str, str]] dtype: str diff --git a/tests/test_io_MultiBand.py b/tests/test_io_MultiBand.py index f72c473e..8371c97d 100644 --- a/tests/test_io_MultiBand.py +++ b/tests/test_io_MultiBand.py @@ -5,10 +5,9 @@ from typing import Dict, Optional, Sequence, Type import attr -import morecantile import pytest -from rio_tiler.constants import WEB_MERCATOR_TMS +from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS from rio_tiler.errors import ExpressionMixingWarning, InvalidExpression, MissingBands from rio_tiler.io import BaseReader, MultiBandReader, Reader from rio_tiler.models import BandStatistics @@ -21,24 +20,12 @@ class BandFileReader(MultiBandReader): """Test MultiBand""" input: str = attr.ib() - tms: morecantile.TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) reader: Type[BaseReader] = attr.ib(init=False, default=Reader) reader_options: Dict = attr.ib(factory=dict) default_bands: Optional[Sequence[str]] = attr.ib(default=None) - minzoom: int = attr.ib() - maxzoom: int = attr.ib() - - @minzoom.default - def _minzoom(self): - return self.tms.minzoom - - @maxzoom.default - def _maxzoom(self): - return self.tms.maxzoom - def __attrs_post_init__(self): """Parse Sceneid and get grid bounds.""" self.bands = sorted( @@ -47,8 +34,9 @@ def __attrs_post_init__(self): with self.reader(self._get_band_url(self.bands[0])) as src: self.bounds = src.bounds self.crs = src.crs - self.minzoom = src.minzoom - self.maxzoom = src.maxzoom + self.transform = src.transform + self.height = src.height + self.width = src.width def _get_band_url(self, band: str) -> str: """Validate band's name and return band's url.""" @@ -59,11 +47,12 @@ def test_MultiBandReader(): """Should work as expected.""" with BandFileReader(PREFIX) as src: assert src.bands == ["band1", "band2"] - assert src.minzoom is not None - assert src.maxzoom is not None - assert src.bounds + minzoom, maxzoom = src.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 7 + assert maxzoom == 9 assert src.bounds assert src.crs + assert src.geographic_bounds(WGS84_CRS) assert sorted(src.parse_expression("band1/band2")) == ["band1", "band2"] diff --git a/tests/test_io_image.py b/tests/test_io_image.py index bfabba13..182166fa 100644 --- a/tests/test_io_image.py +++ b/tests/test_io_image.py @@ -19,8 +19,9 @@ def test_non_geo_image(): """Test ImageReader usage with Non-Geo Images.""" with pytest.warns((NotGeoreferencedWarning)): with ImageReader(NO_GEO) as src: - assert src.minzoom == 0 - assert src.maxzoom == 3 + minzoom, maxzoom = src.get_zooms() + assert minzoom == 0 + assert maxzoom == 3 with pytest.warns((NotGeoreferencedWarning)): with ImageReader(NO_GEO) as src: @@ -98,8 +99,9 @@ def test_non_geo_image(): def test_with_geo_image(): """Test ImageReader usage with Geo Images.""" with ImageReader(GEO) as src: - assert src.minzoom == 0 - assert src.maxzoom == 2 + minzoom, maxzoom = src.get_zooms() + assert minzoom == 0 + assert maxzoom == 2 assert list(src.tms.xy_bounds(0, 0, 2)) == [0, 256, 256, 0] assert list(src.tms.xy_bounds(0, 0, 1)) == [0, 512, 512, 0] diff --git a/tests/test_io_rasterio.py b/tests/test_io_rasterio.py index f5c053cb..22b5e67b 100644 --- a/tests/test_io_rasterio.py +++ b/tests/test_io_rasterio.py @@ -1,11 +1,9 @@ """tests rio_tiler.io.rasterio.Reader""" import os -import warnings from io import BytesIO from typing import Any, Dict -import attr import morecantile import numpy import pytest @@ -62,8 +60,9 @@ def test_spatial_info_valid(): assert not src.dataset.closed assert src.bounds assert src.crs - assert src.minzoom == 5 - assert src.maxzoom == 9 + minzoom, maxzoom = src.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 5 + assert maxzoom == 9 assert src.dataset.closed src = Reader(COG_NODATA) @@ -106,8 +105,7 @@ def test_info_valid(): with Reader(COG_TAGS) as src: meta = src.info() assert meta.bounds - assert meta.minzoom - assert meta.maxzoom + assert meta.crs assert meta.band_descriptions assert meta.dtype == "int16" assert meta.colorinterp == ["gray"] @@ -479,8 +477,9 @@ def test_cog_with_internal_gcps(): with Reader(COG_GCPS) as src: assert isinstance(src.dataset, WarpedVRT) assert src.bounds - assert src.minzoom == 7 - assert src.maxzoom == 10 + minzoom, maxzoom = src.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 7 + assert maxzoom == 10 metadata = src.info() assert metadata.nodata_type == "Alpha" @@ -511,8 +510,9 @@ def test_cog_with_internal_gcps(): with Reader(None, dataset=vrt) as src: assert src.bounds assert isinstance(src.dataset, WarpedVRT) - assert src.minzoom == 7 - assert src.maxzoom == 10 + minzoom, maxzoom = src.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 7 + assert maxzoom == 10 metadata = src.info() assert metadata.nodata_type == "None" @@ -847,8 +847,10 @@ def test_fullEarth(): img = src.tile(127, 42, 7, tilesize=64) assert img.data.shape == (1, 64, 64) - with Reader(COG_EARTH, tms=morecantile.tms.get("EuropeanETRS89_LAEAQuad")) as src: - img = src.tile(0, 0, 1, tilesize=64) + with Reader(COG_EARTH) as src: + img = src.tile( + 0, 0, 1, tilesize=64, tms=morecantile.tms.get("EuropeanETRS89_LAEAQuad") + ) assert img.data.shape == (1, 64, 64) @@ -888,50 +890,50 @@ def test_nonearthbody(): """Reader should work with non-earth dataset.""" EUROPA_SPHERE = CRS.from_proj4("+proj=longlat +R=1560800 +no_defs") - with pytest.warns(UserWarning): - with Reader(COG_EUROPA) as src: - assert src.minzoom == 0 - assert src.maxzoom == 24 + with Reader(COG_EUROPA) as src: + minzoom, maxzoom = src.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 0 + assert maxzoom == 24 - # Warns because of zoom level in WebMercator can't be defined - with pytest.warns(UserWarning) as w: - with Reader(COG_EUROPA, geographic_crs=EUROPA_SPHERE) as src: - assert src.info() - assert len(w) == 2 + with Reader(COG_EUROPA) as src: + assert src.info() - img = src.read() - assert numpy.array_equal(img.data, src.dataset.read(indexes=(1,))) - assert img.width == src.dataset.width - assert img.height == src.dataset.height - assert img.count == src.dataset.count + img = src.read() + assert numpy.array_equal(img.data, src.dataset.read(indexes=(1,))) + assert img.width == src.dataset.width + assert img.height == src.dataset.height + assert img.count == src.dataset.count - img = src.preview() - assert img.bounds == src.bounds + img = src.preview() + assert img.bounds == src.bounds - part = src.part(src.bounds, bounds_crs=src.crs) - assert part.bounds == src.bounds + part = src.part(src.bounds, bounds_crs=src.crs) + assert part.bounds == src.bounds - lon = (src.bounds[0] + src.bounds[2]) / 2 - lat = (src.bounds[1] + src.bounds[3]) / 2 - assert src.point(lon, lat, coord_crs=src.crs).data[0] is not None + lon = (src.bounds[0] + src.bounds[2]) / 2 + lat = (src.bounds[1] + src.bounds[3]) / 2 + assert src.point(lon, lat, coord_crs=src.crs).data[0] is not None - with pytest.warns(UserWarning): - europa_crs = CRS.from_authority("ESRI", 104915) - tms = TileMatrixSet.custom( - crs=europa_crs, - extent=europa_crs.area_of_use.bounds, - matrix_scale=[2, 1], - ) + europa_crs = CRS.from_authority("ESRI", 104915) + tms = TileMatrixSet.custom( + crs=europa_crs, + extent=europa_crs.area_of_use.bounds, + matrix_scale=[2, 1], + ) - with Reader(COG_EUROPA, tms=tms, geographic_crs=EUROPA_SPHERE) as src: + with Reader(COG_EUROPA) as src: assert src.info() - assert src.minzoom == 4 - assert src.maxzoom == 6 + minzoom, maxzoom = src.get_zooms(tms) + assert minzoom == 4 + assert maxzoom == 6 + + bounds = src.geographic_bounds(EUROPA_SPHERE) + assert bounds # Get Tile covering the UL corner bounds = transform_bounds(src.crs, tms.rasterio_crs, *src.bounds) - t = tms._tile(bounds[0], bounds[1], src.minzoom) - img = src.tile(t.x, t.y, t.z) + t = tms._tile(bounds[0], bounds[1], minzoom) + img = src.tile(t.x, t.y, t.z, tms=tms) assert img.height == 256 assert img.width == 256 @@ -958,35 +960,22 @@ def test_nonearth_custom(): geographic_crs=MARS2000_SPHERE, ) - @attr.s - class MarsReader(Reader): - """Use custom geographic CRS.""" - - geographic_crs: rasterio.crs.CRS = attr.ib( - init=False, - default=rasterio.crs.CRS.from_proj4("+proj=longlat +R=3396190 +no_defs"), - ) - - with warnings.catch_warnings(): - with MarsReader(COG_MARS, tms=mars_tms) as src: - assert src.geographic_bounds[0] > -180 + with Reader(COG_MARS) as src: + bounds = src.geographic_bounds(mars_tms.rasterio_geographic_crs) + assert bounds[0] > -180 - with warnings.catch_warnings(): - with Reader( - COG_MARS, - tms=mars_tms, - geographic_crs=rasterio.crs.CRS.from_proj4( - "+proj=longlat +R=3396190 +no_defs" - ), - ) as src: - assert src.geographic_bounds[0] > -180 + minzoom, maxzoom = src.get_zooms(mars_tms) + x, y, z = mars_tms.tile(bounds[0], bounds[1], minzoom) + img = src.tile(x, y, z, tms=mars_tms) + assert img.crs == MARS_MERCATOR def test_tms_tilesize_and_zoom(): """Test the influence of tms tilesize on COG zoom levels.""" with Reader(COG_NODATA) as src: - assert src.minzoom == 5 - assert src.maxzoom == 9 + minzoom, maxzoom = src.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 5 + assert maxzoom == 9 tms_128 = TileMatrixSet.custom( WEB_MERCATOR_TMS.xy_bbox, @@ -995,9 +984,10 @@ def test_tms_tilesize_and_zoom(): tile_width=64, tile_height=64, ) - with Reader(COG_NODATA, tms=tms_128) as src: - assert src.minzoom == 5 - assert src.maxzoom == 11 + with Reader(COG_NODATA) as src: + minzoom, maxzoom = src.get_zooms(tms_128) + assert minzoom == 5 + assert maxzoom == 11 tms_2048 = TileMatrixSet.custom( WEB_MERCATOR_TMS.xy_bbox, @@ -1006,9 +996,10 @@ def test_tms_tilesize_and_zoom(): tile_width=2048, tile_height=2048, ) - with Reader(COG_NODATA, tms=tms_2048) as src: - assert src.minzoom == 5 - assert src.maxzoom == 6 + with Reader(COG_NODATA) as src: + minzoom, maxzoom = src.get_zooms(tms_2048) + assert minzoom == 5 + assert maxzoom == 6 def test_metadata_img(): @@ -1121,8 +1112,9 @@ def test_inverted_latitude(): """Test working with inverted Latitude.""" with pytest.warns(UserWarning): with Reader(COG_INVERTED) as src: - assert src.geographic_bounds[1] < src.geographic_bounds[3] + bounds = src.geographic_bounds(WGS84_CRS) + assert bounds[1] < bounds[3] with pytest.warns(UserWarning): with Reader(COG_INVERTED) as src: - _ = src.tile(0, 0, 0) + _ = src.tile(0, 0, 0, tms=WEB_MERCATOR_TMS) diff --git a/tests/test_io_stac.py b/tests/test_io_stac.py index 02f65bd0..3e49cd7e 100644 --- a/tests/test_io_stac.py +++ b/tests/test_io_stac.py @@ -14,6 +14,7 @@ from rasterio._env import get_gdal_config from rasterio.crs import CRS +from rio_tiler.constants import WEB_MERCATOR_TMS from rio_tiler.errors import ( AssetAsBandError, ExpressionMixingWarning, @@ -54,28 +55,25 @@ def test_fetch_stac(httpx, s3_get): """Test STACReader.""" # Local path with STACReader(STAC_PATH) as stac: - assert stac.minzoom == 0 - assert stac.maxzoom == 24 + minzoom, maxzoom = stac.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 0 + assert maxzoom == 24 assert stac.bounds assert stac.input == STAC_PATH assert stac.assets == ["red", "green", "blue", "lowres"] httpx.assert_not_called() s3_get.assert_not_called() - with STACReader(STAC_PATH, tms=morecantile.tms.get("GNOSISGlobalGrid")) as stac: - assert stac.minzoom == 0 - assert stac.maxzoom == 28 - assert stac.bounds - - with STACReader(STAC_PATH, minzoom=4, maxzoom=8) as stac: - assert stac.minzoom == 4 - assert stac.maxzoom == 8 - assert stac.bounds + with STACReader(STAC_PATH) as stac: + minzoom, maxzoom = stac.get_zooms(morecantile.tms.get("GNOSISGlobalGrid")) + assert minzoom == 0 + assert maxzoom == 28 # Load from dict with STACReader(None, item=item) as stac: - assert stac.minzoom == 0 - assert stac.maxzoom == 24 + minzoom, maxzoom = stac.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 0 + assert maxzoom == 24 assert not stac.input assert stac.assets == ["red", "green", "blue", "lowres"] httpx.assert_not_called() @@ -151,14 +149,9 @@ def raise_for_status(self): def test_projection_extension(): """Test STAC with the projection extension.""" with STACReader(STAC_PATH_PROJ) as stac: - assert stac.minzoom == 6 - assert stac.maxzoom == 7 - assert stac.bounds - assert stac.crs == CRS.from_epsg(32617) - - with STACReader(STAC_PATH_PROJ, minzoom=4, maxzoom=8) as stac: - assert stac.minzoom == 4 - assert stac.maxzoom == 8 + minzoom, maxzoom = stac.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 6 + assert maxzoom == 7 assert stac.bounds assert stac.crs == CRS.from_epsg(32617) @@ -911,7 +904,7 @@ def test_expression_with_wrong_stac_stats(rio): asset_info = stac._get_asset_info("wrongstat") url = asset_info["url"] - with stac.reader(url, tms=stac.tms, **stac.reader_options) as src: + with stac.reader(url, **stac.reader_options) as src: img = src.tile(451, 76, 9, expression="where((b1>0.5),1,0)") assert img.data.shape == (1, 256, 256) assert img.mask.shape == (256, 256) diff --git a/tests/test_io_xarray.py b/tests/test_io_xarray.py index 550ec688..0e06e3c9 100644 --- a/tests/test_io_xarray.py +++ b/tests/test_io_xarray.py @@ -8,6 +8,7 @@ import rioxarray import xarray +from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS from rio_tiler.errors import InvalidGeographicBounds, MissingCRS from rio_tiler.io import XarrayReader @@ -29,8 +30,8 @@ def test_xarray_reader(): data.rio.write_crs("epsg:4326", inplace=True) with XarrayReader(data) as dst: info = dst.info() - assert info.minzoom == 0 - assert info.maxzoom == 0 + assert info.bounds + assert info.crs assert info.band_metadata == [("b1", {})] assert info.band_descriptions == [("b1", "2022-01-01T00:00:00.000000000")] assert info.height == 33 @@ -38,6 +39,12 @@ def test_xarray_reader(): assert info.count == 1 assert info.attrs + minzoom, maxzoom = dst.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 0 + assert maxzoom == 0 + + assert dst.geographic_bounds(WGS84_CRS) + with XarrayReader(data) as dst: img = dst.tile(0, 0, 0) assert img.count == 1 @@ -123,9 +130,9 @@ def test_xarray_reader(): data.rio.write_crs("epsg:4326", inplace=True) with XarrayReader(data) as dst: - info = dst.info() - assert info.minzoom == 5 - assert info.maxzoom == 7 + minzoom, maxzoom = dst.get_zooms(WEB_MERCATOR_TMS) + assert minzoom == 5 + assert maxzoom == 7 def test_xarray_reader_external_nodata():