diff --git a/CHANGES.md b/CHANGES.md index 0fed32de..f512cca5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,68 @@ # 3.0.0 (TDB) +* add `crs` property in `rio_tiler.io.base.SpatialMixin` (https://github.com/cogeotiff/rio-tiler/pull/429) +* add `geographic_bounds` in `rio_tiler.io.base.SpatialMixin` to return bounds in WGS84 (https://github.com/cogeotiff/rio-tiler/pull/429) + +```python +from rio_tiler.io import COGReader + +with COGReader("https://rio-tiler-dev.s3.amazonaws.com/data/fixtures/cog.tif") as cog: + print(cog.bounds) + >> (373185.0, 8019284.949381611, 639014.9492102272, 8286015.0) + + print(cog.crs) + >> "EPSG:32621" + + print(cog.geographic_bounds) + >> (-61.28762442711404, 72.22979795551834, -52.301598718454485, 74.66298001264106) +``` + +* Allow errors to be ignored when trying to find `zooms` for dataset in `rio_tiler.io.COGReader`. If we're not able to find the zooms in selected TMS, COGReader will defaults to the min/max zooms of the TMS (https://github.com/cogeotiff/rio-tiler/pull/429) + +```python +from pyproj import CRS +from morecantile import TileMatrixSet + +from rio_tiler.io import COGReader + +# For a non-earth dataset there is no available transformation from its own CRS and the default WebMercator TMS CRS. +with COGReader("https://rio-tiler-dev.s3.amazonaws.com/data/fixtures/cog_nonearth.tif") as cog: + >> UserWarning: Cannot dertermine min/max zoom based on dataset informations, will default to TMS min/max zoom. + + print(cog.minzoom) + >> 0 + + print(cog.maxzoom) + >> 24 + +# if we use a `compatible TMS` then we don't get warnings +europa_crs = CRS.from_authority("ESRI", 104915) +europa_tms = TileMatrixSet.custom( + crs=europa_crs, + extent=europa_crs.area_of_use.bounds, + matrix_scale=[2, 1], +) +with COGReader( + "https://rio-tiler-dev.s3.amazonaws.com/data/fixtures/cog_nonearth.tif", + tms=europa_tms, +) as cog: + print(cog.minzoom) + >> 4 + + print(cog.maxzoom) + >> 6 +``` + +* compare dataset bounds and tile bounds in TMS crs in `rio_tiler.io.base.SpatialMixin.tile_exists` method to allow dataset and TMS not compatible with WGS84 crs (https://github.com/cogeotiff/rio-tiler/pull/429) + **breaking changes** * update morecantile requirement to version >=3.0 (https://github.com/cogeotiff/rio-tiler/pull/418) * remove python 3.6 support (https://github.com/cogeotiff/rio-tiler/pull/418) * remove `max_size` defaults for `COGReader.part` and `COGReader.feature`, which will now default to full resolution reading. -* Deprecate `.metadata` methods (https://github.com/cogeotiff/rio-tiler/pull/423) +* deprecate `.metadata` methods (https://github.com/cogeotiff/rio-tiler/pull/423) +* remove `rio_tiler.io.base.SpatialMixin.spatial_info` and `rio_tiler.io.base.SpatialMixin.center` properties (https://github.com/cogeotiff/rio-tiler/pull/429) +* `rio_tiler.io.base.SpatialMixin.bounds` should now be in dataset's CRS (not in `WGS84`) (https://github.com/cogeotiff/rio-tiler/pull/429) * Use `RIO_TILER_MAX_THREADS` environment variable instead of `MAX_THREADS` (author @rodrigoalmeida94, https://github.com/cogeotiff/rio-tiler/pull/432) # 2.1.3 (2021-09-14) diff --git a/docs/advanced/custom_readers.md b/docs/advanced/custom_readers.md index fb2b9ad5..e121a340 100644 --- a/docs/advanced/custom_readers.md +++ b/docs/advanced/custom_readers.md @@ -13,47 +13,23 @@ Main `rio_tiler.io` Abstract Base Class. ##### Minimal Arguments -- **tms**: morecantile.TileMatrixSet (default is set to WebMercatorQuad). The TileMatrixSet define which default projection and map grid the reader uses. +- **tms**: The TileMatrixSet define which default projection and map grid the reader uses. Defaults to WebMercatorQuad. +- **minzoom**: Dataset's minzoom. Not in the `__init__` method. +- **maxzoom**: Dataset's maxzoom. Not in the `__init__` method. +- **bounds**: Dataset's bounding box. Not in the `__init__` method. +- **crs**: dataset's crs. Not in the `__init__` method. -- **bounds**: bounding box of the dataset. Not in the `init` method. -- **minzoom**: dataset minzoom. Not in the `init` method. -- **maxzoom**: dataset maxzoom. Not in the `init` method. +!!! important + BaseClass Arguments outside the `__init__` method **HAVE TO** be set in the `__attrs_post_init__` step. -Class arguments set to be define outside the `init` method can be set in the `__attrs_post_init__` step. +#### Methods -Example: -```python - -@attr.s -class Reader(BaseReader): - - filepath: str = attr.ib() # Required argument - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - - # We can overwrite the baseclass attribute definition - minzoom: int = attr.ib(default=WEB_MERCATOR_TMS.minzoom) - maxzoom: int = attr.ib(default=WEB_MERCATOR_TMS.maxzoom) - - bounds: Tuple[float, float, float, float] = attr.ib(init=False) - dataset: rasterio.io.DatasetReader = attr.ib(init=False) - - def __attrs_post_init__(self): - # Set the dataset variable - self.dataset = rasterio.open(self.filepath) - - # Set bounds variable - self.bounds = transform_bounds( - self.dataset.crs, constants.WGS84_CRS, *self.dataset.bounds, densify_pts=21 - ) - ... -``` +- **tile_exists**: Check if a given tile (for the input TMS) intersect the dataset bounds. +- **metadata**: returns info + stats (`rio_tiler.models.Metadata`) ##### Properties -- **center**: dataset center (calculated from bounds and minzoom). -- **spatial_info**: bounds + zoom info. - -Those properties will be added by default in every readers (because bounds and zooms info are part of the BaseReader definition). +- **geographic_bounds**: dataset's bounds in WGS84 crs (calculated from `self.bounds` and `self.crs`). ##### Abstract Methods @@ -61,7 +37,6 @@ Abstract methods, are mehtod that **HAVE TO** be implemented in the subclass. - **info**: returns dataset info (`rio_tiler.models.Info`) - **stats**: returns dataset array statistric (`Dict[str, rio_tiler.models.ImageStatistics]`) -- **metadata**: returns info + stats (`rio_tiler.models.Metadata`) - **tile**: reads data for a specific XYZ slippy map indexes (`rio_tiler.models.ImageData`) - **part**: reads specific part of a dataset (`rio_tiler.models.ImageData`) - **preview**: creates an overview of a dataset (`rio_tiler.models.ImageData`) @@ -102,6 +77,8 @@ class AssetFileReader(MultiBaseReader): reader: Type[BaseReader] = attr.ib(default=COGReader) reader_options: Dict = attr.ib(factory=dict) tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) + minzoom: int = attr.ib(default=None) + maxzoom: int = attr.ib(default=None) def __attrs_post_init__(self): """Parse Sceneid and get grid bounds.""" @@ -110,8 +87,13 @@ class AssetFileReader(MultiBaseReader): ) with self.reader(self._get_asset_url(self.assets[0])) as cog: self.bounds = cog.bounds - self.minzoom = cog.minzoom - self.maxzoom = cog.maxzoom + self.crs = cog.crs + + if self.minzoom is None: + self.minzoom = cog.minzoom + + if self.maxzoom is None: + self.maxzoom = cog.maxzoom def _get_asset_url(self, band: str) -> str: """Validate band's name and return band's url.""" @@ -129,10 +111,9 @@ with AssetFileReader("my_dir/", "scene_") as cr: >>> ['b1', 'b2'] assert isinstance(info["b1"], Info) - print(info["b1"].dict(exclude_none=True)) + print(info["b1"].json(exclude_none=True)) >>> { - 'bounds': (-11.979244865430259, 24.296321392464325, -10.874546803397614, 25.304623891542263), - 'center': (-11.426895834413937, 24.800472642003292, 7), + 'bounds': [-11.979244865430259, 24.296321392464325, -10.874546803397614, 25.304623891542263], 'minzoom': 7, 'maxzoom': 9, 'band_metadata': [('1', {})], @@ -176,6 +157,8 @@ class BandFileReader(MultiBandReader): reader: Type[BaseReader] = attr.ib(default=COGReader) reader_options: Dict = attr.ib(factory=dict) tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) + minzoom: int = attr.ib(default=None) + maxzoom: int = attr.ib(default=None) def __attrs_post_init__(self): """Parse Sceneid and get grid bounds.""" @@ -184,8 +167,13 @@ class BandFileReader(MultiBandReader): ) with self.reader(self._get_band_url(self.bands[0])) as cog: self.bounds = cog.bounds - self.minzoom = cog.minzoom - self.maxzoom = cog.maxzoom + self.crs = cog.crs + + if self.minzoom is None: + self.minzoom = cog.minzoom + + if self.maxzoom is None: + self.maxzoom = cog.maxzoom def _get_band_url(self, band: str) -> str: """Validate band's name and return band's url.""" @@ -197,10 +185,9 @@ with BandFileReader("my_dir/", "scene_") as cr: print(cr.bands) >>> ['b1', 'b2'] - print(cr.info(bands=("b1", "b2")).dict(exclude_none=True)) + print(cr.info(bands=("b1", "b2")).json(exclude_none=True)) >>> { - 'bounds': (-11.979244865430259, 24.296321392464325, -10.874546803397614, 25.304623891542263), - 'center': (-11.426895834413937, 24.800472642003292, 7), + 'bounds': [-11.979244865430259, 24.296321392464325, -10.874546803397614, 25.304623891542263], 'minzoom': 7, 'maxzoom': 9, 'band_metadata': [('b1', {}), ('b2', {})], @@ -268,7 +255,7 @@ with CustomSTACReader("https://canada-spot-ortho.s3.amazonaws.com/canada_spot_or >>> rasterio.io.DatasetReader >>> "https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/s5_11055_6057_20070622_p10_1_lcc00_cog.tif" >>> 0 ->>> (-111.87793996076493, 60.48627186654449, -109.94924666908423, 61.42036313093244) +>>> (-869900.0, 1370200.0, -786360.0, 1453180.0) ``` In this `CustomSTACReader`, we are using a custom path `schema` in form of `{item-url}:{asset-name}`. When creating an instance of `CustomSTACReader`, we will do the following: @@ -277,3 +264,81 @@ In this `CustomSTACReader`, we are using a custom path `schema` in form of `{ite 2. Fetch and parse the STAC item 3. Construct a new `filename` using the asset full url. 4. Fall back to the regular `COGReader` initialization (using `super().__attrs_post_init__()`) + + +## Simple Reader + + +```python +from typing import Any, Dict + +import attr +import rasterio +from rasterio.io import DatasetReader +from rio_tiler.io import BaseReader +from rio_tiler.models import Info, ImageStatistics, ImageData +from morecantile import TileMatrixSet + +from rio_tiler.constants import BBox, WEB_MERCATOR_TMS + +@attr.s +class Reader(BaseReader): + + dataset: DatasetReader = attr.ib() + + # We force tms to be outside the class __init__ + tms: TileMatrixSet = attr.ib(init=False, default=WEB_MERCATOR_TMS) + + # We can overwrite the baseclass attribute definition and set default + minzoom: int = attr.ib(init=False, default=WEB_MERCATOR_TMS.minzoom) + maxzoom: int = attr.ib(init=False, default=WEB_MERCATOR_TMS.maxzoom) + + def __attrs_post_init__(self): + # Set bounds and crs variable + self.bounds = self.dataset.bounds + self.crs = self.dataset.crs + + # implement all mandatory methods + def info(self) -> Info: + raise NotImplemented + + def stats(self, pmin: float = 2.0, pmax: float = 98.0, **kwargs: Any) -> Dict[str, ImageStatistics]: + raise NotImplemented + + def part(self, bbox: BBox, **kwargs: Any) -> ImageData: + raise NotImplemented + + def preview(self, **kwargs: Any) -> ImageData: + raise NotImplemented + + def point(self, lon: float, lat: float, **kwargs: Any) -> List: + raise NotImplemented + + def feature(self, shape: Dict, **kwargs: Any) -> ImageData: + raise NotImplemented + + def tile(self, tile_x: int, tile_y: int, tile_z: int, **kwargs: Any) -> ImageData: + if not self.tile_exists(tile_x, tile_y, tile_z): + raise TileOutsideBounds( + f"Tile {tile_z}/{tile_x}/{tile_y} is outside bounds" + ) + + tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) + + data, mask = reader.part( + self.dataset, + tile_bounds, + width=256, + height=256, + bounds_crs=tms.rasterio_crs, + dst_crs=tms.rasterio_crs, + **kwargs, + ) + return ImageData( + data, mask, bounds=tile_bounds, crs=tms.rasterio_crs + ) + +with rasterio.open("file.tif") as src: + with Reader(src) as cog: + img = cog.tile(1, 1, 1) +``` diff --git a/docs/advanced/dynamic_tiler.md b/docs/advanced/dynamic_tiler.md index 608d46e0..6a45b7e3 100644 --- a/docs/advanced/dynamic_tiler.md +++ b/docs/advanced/dynamic_tiler.md @@ -76,7 +76,7 @@ def tile( ): """Handle tile requests.""" with COGReader(url) as cog: - img = cog.tile(x, y, z, tilesize=256) + img = cog.tile(x, y, z) content = img.render(img_format="PNG", **img_profiles.get("png")) return Response(content, media_type="image/png") @@ -92,8 +92,7 @@ def tilejson( with COGReader(url) as cog: return { - "bounds": cog.bounds, - "center": cog.center, + "bounds": cog.geographic_bounds, "minzoom": cog.minzoom, "maxzoom": cog.maxzoom, "name": os.path.basename(url), diff --git a/docs/examples/Using-nonEarth-dataset.ipynb b/docs/examples/Using-nonEarth-dataset.ipynb new file mode 100644 index 00000000..7a0730a7 --- /dev/null +++ b/docs/examples/Using-nonEarth-dataset.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Non Earth dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Requirements\n", + "\n", + "To be able to run this notebook you'll need the following requirements:\n", + "- rio-tiler~= 3.0\n", + "- ipyleaflet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install rio-tiler\n", + "# !pip install ipyleaflet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from ipyleaflet import (\n", + " Map,\n", + " basemaps,\n", + " basemap_to_tiles,\n", + " TileLayer,\n", + " WMSLayer,\n", + " GeoJSON,\n", + " projections\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# For this DEMO we will use this file\n", + "src_path = \"https://asc-jupiter.s3-us-west-2.amazonaws.com/europa/galileo_voyager/controlled_mosaics/11ESCOLORS01-02_GalileoSSI_Equi-cog.tif\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tile Server\n", + "\n", + "For this demo, we need to create a minimal tile server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from concurrent import futures\n", + "\n", + "from tornado import web\n", + "from tornado import gen\n", + "from tornado.httpserver import HTTPServer\n", + "from tornado.concurrent import run_on_executor\n", + "\n", + "from rio_tiler.io import COGReader\n", + "from rio_tiler.errors import TileOutsideBounds\n", + "from rio_tiler.profiles import img_profiles\n", + "\n", + "from pyproj import CRS\n", + "from morecantile import TileMatrixSet\n", + "\n", + "# Create a CUSTOM TMS using the europa ESRI:104915 projection\n", + "europa_crs = CRS.from_authority(\"ESRI\", 104915)\n", + "europa_tms = TileMatrixSet.custom(\n", + " crs=europa_crs, extent=europa_crs.area_of_use.bounds, matrix_scale=[2, 1],\n", + ")\n", + "\n", + "class TileServer:\n", + " def __init__(self, src_path):\n", + " \"\"\"Initialize Tornado app.\"\"\"\n", + " self.server = None\n", + " self.app = web.Application([\n", + " (r\"^/tiles/(\\d+)/(\\d+)/(\\d+)\", TileHandler, {\"url\": src_path}),\n", + " ])\n", + "\n", + " def start(self):\n", + " \"\"\"Start tile server.\"\"\"\n", + " self.server = HTTPServer(self.app)\n", + " self.server.listen(8080)\n", + " \n", + " def stop(self):\n", + " \"\"\"Stop tile server.\"\"\"\n", + " if self.server:\n", + " self.server.stop()\n", + "\n", + "\n", + "class TileHandler(web.RequestHandler):\n", + " \"\"\"Tile requests handler.\"\"\"\n", + "\n", + " executor = futures.ThreadPoolExecutor(max_workers=16)\n", + "\n", + " def initialize(self, url):\n", + " \"\"\"Initialize tiles handler.\"\"\"\n", + " self.url = url\n", + "\n", + " @run_on_executor\n", + " def _get_tile(self, z, x, y):\n", + "\n", + " try:\n", + " with COGReader(self.url, tms=europa_tms) as cog:\n", + " data = cog.tile(x, y, z)\n", + " except TileOutsideBounds:\n", + " raise web.HTTPError(404)\n", + "\n", + " image = data.post_process(in_range=((0, 0.5),))\n", + "\n", + " prof = img_profiles.get(\"PNG\", {})\n", + " return image.render(img_format=\"PNG\", **prof)\n", + "\n", + " @gen.coroutine\n", + " def get(self, z, x, y):\n", + " \"\"\"Retunrs tile data and header.\"\"\"\n", + " self.set_header(\"Access-Control-Allow-Origin\", \"*\")\n", + " self.set_header(\"Access-Control-Allow-Methods\", \"GET\")\n", + " self.set_header(\"Content-Type\", \"image/png\")\n", + " self.set_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate\")\n", + " res = yield self._get_tile(int(z), int(x), int(y))\n", + " self.write(res)\n", + "\n", + "\n", + "ts = TileServer(src_path)\n", + "ts.start()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bounds = (129.36834223297478, 13.985559117409744, 138.90253908503576, 23.13673177454536)\n", + "\n", + "m = Map(\n", + " center=(\n", + " (bounds[1] + bounds[3]) / 2,\n", + " (bounds[0] + bounds[2]) / 2\n", + " ),\n", + " zoom=4,\n", + " basemap={},\n", + " crs=projections.EPSG4326, # HACK: the europa TMS is in degree and covers -180, -90, 180, 90 like the WGS84\n", + ")\n", + "\n", + "layer = TileLayer(\n", + " url=\"http://127.0.0.1:8080/tiles/{z}/{x}/{y}\",\n", + " min_zoom=4,\n", + " max_zoom=6,\n", + " opacity=1,\n", + ")\n", + "m.add_layer(layer)\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ts.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/models.md b/docs/models.md index d6e1f7c4..55e00646 100644 --- a/docs/models.md +++ b/docs/models.md @@ -163,57 +163,7 @@ Note: Starting with `rio-tiler==2.1`, when the output datatype is not valid for ## Others -Readers methods (`spatial_info`, `info`, `metadata` and `stats`) returning metadata like results return [pydantic](https://pydantic-docs.helpmanual.io) models to make sure the values are valids. - -### SpatialInfo - -```python -from rio_tiler.io import COGReader -from rio_tiler.models import SpatialInfo - -# Schema -print(SpatialInfo.schema()) ->>> { - 'title': 'SpatialInfo', - 'description': 'Dataset SpatialInfo', - 'type': 'object', - 'properties': { - 'bounds': {'title': 'Bounds', 'type': 'array', 'items': {}}, - 'center': { - 'title': 'Center', - 'type': 'array', - 'items': [ - {'anyOf': [{'type': 'number'}, {'type': 'integer'}]}, - {'anyOf': [{'type': 'number'}, {'type': 'integer'}]}, - {'type': 'integer'} - ] - }, - 'minzoom': {'title': 'Minzoom', 'type': 'integer'}, - 'maxzoom': {'title': 'Maxzoom', 'type': 'integer'} - }, - 'required': ['bounds', 'center', 'minzoom', 'maxzoom'] -} - -# Example -with COGReader( - "http://oin-hotosm.s3.amazonaws.com/5a95f32c2553e6000ce5ad2e/0/10edab38-1bdd-4c06-b83d-6e10ac532b7d.tif" -) as cog: - spatial_info = cog.spatial_info - -print(spatial_info["minzoom"]) ->>> 15 - -print(spatial_info.minzoom) ->>> 15 - -print(spatial_info.dict()) ->>> { - 'bounds': (-61.28700187663819, 15.53775679445058, -61.27877967704676, 15.542486503997605), - 'center': (-61.28289077684248, 15.540121649224092, 15), - 'minzoom': 15, - 'maxzoom': 21 -} -``` +Readers methods (`info`, `metadata` and `stats`) returning metadata like results return [pydantic](https://pydantic-docs.helpmanual.io) models to make sure the values are valids. ### Info @@ -224,34 +174,132 @@ from rio_tiler.models import Info # Schema print(Info.schema()) >>> { - 'title': 'Info', - 'description': 'Dataset Info.', - 'type': 'object', - 'properties': { - 'bounds': {'title': 'Bounds', 'type': 'array', 'items': {}}, - 'center': { - 'title': 'Center', - 'type': 'array', - 'items': [ - {'anyOf': [{'type': 'number'}, {'type': 'integer'}]}, - {'anyOf': [{'type': 'number'}, {'type': 'integer'}]}, - {'type': 'integer'} + "title": "Info", + "description": "Dataset Info.", + "type": "object", + "properties": { + "bounds": { + "title": "Bounds", + "type": "array", + "items": [ + { + "title": "Left" + }, + { + "title": "Bottom" + }, + { + "title": "Right" + }, + { + "title": "Top" + } ] }, - 'minzoom': {'title': 'Minzoom', 'type': 'integer'}, - 'maxzoom': {'title': 'Maxzoom', 'type': 'integer'}, - 'band_metadata': {'title': 'Band Metadata', 'type': 'array', 'items': {'type': 'array', 'items': [{'type': 'string'}, {'type': 'object'}]}}, - 'band_descriptions': {'title': 'Band Descriptions', 'type': 'array', 'items': {'type': 'array', 'items': [{'type': 'string'}, {'type': 'string'}]}}, - 'dtype': {'title': 'Dtype', 'type': 'string'}, - 'nodata_type': {'$ref': '#/definitions/NodataTypes'}, - 'colorinterp': {'title': 'Colorinterp', 'type': 'array', 'items': {'type': 'string'}}, - 'scale': {'title': 'Scale', 'type': 'number'}, - 'offset': {'title': 'Offset', 'type': 'number'}, - 'colormap': {'title': 'Colormap', 'type': 'object', 'additionalProperties': {'type': 'array', 'items': [{'type': 'integer'}, {'type': 'integer'}, {'type': 'integer'}, {'type': 'integer'}]}} + "minzoom": { + "title": "Minzoom", + "type": "integer" + }, + "maxzoom": { + "title": "Maxzoom", + "type": "integer" + }, + "band_metadata": { + "title": "Band Metadata", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } + }, + "band_descriptions": { + "title": "Band Descriptions", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + } + }, + "dtype": { + "title": "Dtype", + "type": "string" + }, + "nodata_type": { + "$ref": "#/definitions/NodataTypes" + }, + "colorinterp": { + "title": "Colorinterp", + "type": "array", + "items": { + "type": "string" + } + }, + "scale": { + "title": "Scale", + "type": "number" + }, + "offset": { + "title": "Offset", + "type": "number" + }, + "colormap": { + "title": "Colormap", + "type": "object", + "additionalProperties": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + } }, - 'required': ['bounds', 'center', 'minzoom', 'maxzoom', 'band_metadata', 'band_descriptions', 'dtype', 'nodata_type'], - 'definitions': { - 'NodataTypes': {'title': 'NodataTypes', 'description': 'rio-tiler Nodata types.', 'enum': ['Alpha', 'Mask', 'Internal', 'Nodata', 'None'], 'type': 'string'} + "required": [ + "bounds", + "minzoom", + "maxzoom", + "band_metadata", + "band_descriptions", + "dtype", + "nodata_type" + ], + "definitions": { + "NodataTypes": { + "title": "NodataTypes", + "description": "rio-tiler Nodata types.", + "enum": [ + "Alpha", + "Mask", + "Internal", + "Nodata", + "None" + ], + "type": "string" + } } } @@ -267,22 +315,21 @@ print(info["nodata_type"]) print(info.nodata_type) >>> "None" -print(info.dict(exclude_none=True)) +print(info.json(exclude_none=True)) >>> { - 'bounds': (-61.28700187663819, 15.53775679445058, -61.27877967704676, 15.542486503997605), - 'center': (-61.28289077684248, 15.540121649224092, 15), - 'minzoom': 15, - 'maxzoom': 21, + 'bounds': [-61.287001876638215, 15.537756794450583, -61.27877967704677, 15.542486503997608], + 'minzoom': 16, + 'maxzoom': 22, 'band_metadata': [('1', {}), ('2', {}), ('3', {})], 'band_descriptions': [('1', ''), ('2', ''), ('3', '')], 'dtype': 'uint8', 'nodata_type': 'None', 'colorinterp': ['red', 'green', 'blue'], - 'driver': "GTiff', 'count': 3, - 'width': 1000, - 'height': 2000, - 'overviews': [2, 4, 8, 16, 32], + 'driver': 'GTiff', + 'height': 11666, + 'overviews': [2, 4, 8, 16, 32, 64], + 'width': 19836 } ``` @@ -305,42 +352,98 @@ print(Metadata.schema()) "title": "Bounds", "type": "array", "items": [ - {"title": "Left"}, {"title": "Bottom"}, {"title": "Right"}, {"title": "Top"} + { + "title": "Left" + }, + { + "title": "Bottom" + }, + { + "title": "Right" + }, + { + "title": "Top" + } ] }, - "center": { - "title": "Center", - "type": "array", - "items": [ - {"anyOf": [{"type": "number"}, {"type": "integer"}]}, - {"anyOf": [{"type": "number"}, {"type": "integer"}]}, - {"type": "integer"} - ] + "minzoom": { + "title": "Minzoom", + "type": "integer" + }, + "maxzoom": { + "title": "Maxzoom", + "type": "integer" }, - "minzoom": {"title": "Minzoom", "type": "integer"}, - "maxzoom": {"title": "Maxzoom", "type": "integer"}, "band_metadata": { "title": "Band Metadata", "type": "array", - "items": {"type": "array", "items": [{"type": "string"},{"type": "object"}]} + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } }, "band_descriptions": { "title": "Band Descriptions", "type": "array", - "items": {"type": "array", "items": [{"type": "string"}, {"type": "string"}]} + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + } + }, + "dtype": { + "title": "Dtype", + "type": "string" + }, + "nodata_type": { + "$ref": "#/definitions/NodataTypes" + }, + "colorinterp": { + "title": "Colorinterp", + "type": "array", + "items": { + "type": "string" + } + }, + "scale": { + "title": "Scale", + "type": "number" + }, + "offset": { + "title": "Offset", + "type": "number" }, - "dtype": {"title": "Dtype", "type": "string" }, - "nodata_type": {"$ref": "#/definitions/NodataTypes"}, - "colorinterp": {"title": "Colorinterp","type": "array", "items": {"type": "string"}}, - "scale": {"title": "Scale", "type": "number"}, - "offset": {"title": "Offset", "type": "number"}, "colormap": { "title": "Colormap", "type": "object", "additionalProperties": { "type": "array", "items": [ - {"type": "integer"}, {"type": "integer"}, {"type": "integer"}, {"type": "integer"} + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + } ] } }, @@ -353,13 +456,26 @@ print(Metadata.schema()) } }, "required": [ - "bounds", "center", "minzoom", "maxzoom", "band_metadata", "band_descriptions", "dtype", "nodata_type", "statistics" + "bounds", + "minzoom", + "maxzoom", + "band_metadata", + "band_descriptions", + "dtype", + "nodata_type", + "statistics" ], "definitions": { "NodataTypes": { "title": "NodataTypes", "description": "rio-tiler Nodata types.", - "enum": ["Alpha", "Mask", "Internal", "Nodata", "None"], + "enum": [ + "Alpha", + "Mask", + "Internal", + "Nodata", + "None" + ], "type": "string" }, "ImageStatistics": { @@ -370,22 +486,80 @@ print(Metadata.schema()) "percentiles": { "title": "Percentiles", "type": "array", - "items": {"anyOf": [{"type": "number"}, {"type": "integer"}]} + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + } + ] + } + }, + "min": { + "title": "Min", + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + } + ] + }, + "max": { + "title": "Max", + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + } + ] + }, + "std": { + "title": "Std", + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + } + ] }, - "min": {"title": "Min", "anyOf": [{"type": "number"}, {"type": "integer"}]}, - "max": {"title": "Max", "anyOf": [{"type": "number"}, {"type": "integer"}]}, - "std": {"title": "Std", "anyOf": [{"type": "number"}, {"type": "integer"}]}, "histogram": { "title": "Histogram", "type": "array", "items": { "type": "array", - "items": {"anyOf": [{"type": "number"}, {"type": "integer"}]} + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + } + ] + } } }, - "valid_percent": {"title": "Valid Percent", "type": "number"} + "valid_percent": { + "title": "Valid Percent", + "type": "number" + } }, - "required": ["percentiles", "min", "max", "std", "histogram", "valid_percent"] + "required": [ + "percentiles", + "min", + "max", + "std", + "histogram", + "valid_percent" + ] } } } @@ -402,12 +576,11 @@ print(metadata["statistics"]["1"]["min"]) print(metadata.statistics["1"].min) >>> 0.0 -print(metadata.dict(exclude_none=True)) +print(metadata.json(exclude_none=True)) >>> { - 'bounds': (-61.28700187663819, 15.53775679445058, -61.27877967704676, 15.542486503997605), - 'center': (-61.28289077684248, 15.540121649224092, 15), - 'minzoom': 15, - 'maxzoom': 21, + 'bounds': [-61.287001876638215, 15.537756794450583, -61.27877967704677, 15.542486503997608], + 'minzoom': 16, + 'maxzoom': 22, 'band_metadata': [('1', {}), ('2', {}), ('3', {})], 'band_descriptions': [('1', ''), ('2', ''), ('3', '')], 'dtype': 'uint8', @@ -420,9 +593,10 @@ print(metadata.dict(exclude_none=True)) 'max': 255.0, 'std': 59.261322978176324, 'histogram': [ - [100540.0,43602.0, 87476.0, 112587.0, 107599.0, 73453.0, 43623.0, 21971.0, 15006.0, 11615.0], + [100540.0, 43602.0, 87476.0, 112587.0, 107599.0, 73453.0, 43623.0, 21971.0, 15006.0, 11615.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0] - ] + ], + 'valid_percent': 100.0 }, '2': { 'percentiles': [0.0, 231.0], @@ -432,7 +606,8 @@ print(metadata.dict(exclude_none=True)) 'histogram': [ [95196.0, 33243.0, 67186.0, 116180.0, 128328.0, 82649.0, 45167.0, 22637.0, 13985.0, 12901.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0] - ] + ], + 'valid_percent': 100.0 }, '3': { 'percentiles': [0.0, 232.0], @@ -442,10 +617,15 @@ print(metadata.dict(exclude_none=True)) 'histogram': [ [122393.0, 94783.0, 136757.0, 100639.0, 63487.0, 32661.0, 24458.0, 16910.0, 11900.0, 13484.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0] - ] + ], + 'valid_percent': 100.0 } }, - 'valid_percent': 0.9 + 'count': 3, + 'driver': 'GTiff', + 'height': 11666, + 'overviews': [2, 4, 8, 16, 32, 64], + 'width': 19836 } ``` diff --git a/docs/readers.md b/docs/readers.md index fa5f2f1a..6d5b99a8 100644 --- a/docs/readers.md +++ b/docs/readers.md @@ -3,13 +3,14 @@ #### Properties -- **dataset**: Return the rasterio dataset -- **colormap**: Return the dataset's internal colormap -- **minzoom**: Return minimum Mercator Zoom -- **maxzoom**: Return maximum Mercator Zoom -- **bounds**: Return the dataset bounds in WGS84 -- **center**: Return the center of the dataset + minzoom -- **spatial_info**: Return the bounds, center and zoom infos (`rio_tiler.models.SpatialInfo`) +- **dataset**: rasterio openned dataset +- **tms**: morecantile TileMatrixSet used for tile reading +- **minzoom**: dataset's minimum zoom level (for input tms) +- **maxzoom**: dataset's maximum zoom level (for input tms) +- **bounds**: dataset's bounds (in dataset crs) +- **crs**: dataset's crs +- **geographic_bounds**: dataset's bounds in WGS84 +- **colormap**: dataset's internal colormap #### Methods @@ -34,7 +35,7 @@ with COGReader("myfile.tif") as cog: assert img.data.count == 1 # With expression -with COGReader("myfile.tif"s) as cog: +with COGReader("myfile.tif") as cog: img = cog.read(expression="B1/B2") assert img.data.count == 1 ``` @@ -206,7 +207,6 @@ with COGReader("myfile.tif") as cog: print(info.dict(exclude_none=True)) >>> { "bounds": [-119.05915661478785, 13.102845359730287, -84.91821332299578, 33.995073647795806], - "center": [-101.98868496889182, 23.548959503763047, 3], "minzoom": 3, "maxzoom": 12, "band_metadata": [["1", {}]], @@ -272,7 +272,6 @@ with COGReader("myfile.tif") as cog: print(metadata.dict(exclude_none=True)) >>> { "bounds": [-119.05915661478785, 13.102845359730287, -84.91821332299578, 33.995073647795806], - "center": [-101.98868496889182, 23.548959503763047, 3], "minzoom": 3, "maxzoom": 12, "band_metadata": [["1", {}]], @@ -339,8 +338,10 @@ with STACReader( exclude_assets={"thumbnail"} ) as stac: print(stac.bounds) + print(stac.geographic_bounds) print(stac.assets) +>>> [23.293255090449595, 31.505183020453355, 24.296453548295318, 32.51147809805106] >>> [23.293255090449595, 31.505183020453355, 24.296453548295318, 32.51147809805106] >>> ['overview', 'visual', 'B01', 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B08', 'B8A', 'B09', 'B11', 'B12', 'AOT', 'WVP', 'SCL'] diff --git a/rio_tiler/io/base.py b/rio_tiler/io/base.py index ecfb14c8..654c9c5a 100644 --- a/rio_tiler/io/base.py +++ b/rio_tiler/io/base.py @@ -8,8 +8,10 @@ import attr from morecantile import Tile, TileMatrixSet +from rasterio.crs import CRS +from rasterio.warp import transform_bounds -from ..constants import WEB_MERCATOR_TMS, BBox +from ..constants import WEB_MERCATOR_TMS, WGS84_CRS, BBox from ..errors import ( ExpressionMixingWarning, MissingAssets, @@ -17,7 +19,7 @@ TileOutsideBounds, ) from ..expression import apply_expression -from ..models import ImageData, ImageStatistics, Info, Metadata, SpatialInfo +from ..models import ImageData, ImageStatistics, Info, Metadata from ..tasks import multi_arrays, multi_values @@ -27,35 +29,35 @@ class SpatialMixin: Attributes: tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`. - bbox (tuple): Dataset bounds (left, bottom, right, top). **READ ONLY attribute**. - minzoom (int): Overwrite Min Zoom level. **READ ONLY attribute**. - maxzoom (int): Overwrite Max Zoom level. **READ ONLY attribute**. + minzoom (int): Dataset Min Zoom level. **Not in __init__**. + maxzoom (int): Dataset Max Zoom level. **Not in __init__**. + bounds (tuple): Dataset bounds (left, bottom, right, top). **Not in __init__**. + crs (rasterio.crs.CRS): Dataset crs. **Not in __init__**. """ tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - bounds: BBox = attr.ib(init=False) minzoom: int = attr.ib(init=False) maxzoom: int = attr.ib(init=False) - @property - def center(self) -> Tuple[float, float, int]: - """Dataset center + minzoom.""" - return ( - (self.bounds[0] + self.bounds[2]) / 2, - (self.bounds[1] + self.bounds[3]) / 2, - self.minzoom, - ) + bounds: BBox = attr.ib(init=False) + crs: CRS = attr.ib(init=False) @property - def spatial_info(self) -> SpatialInfo: - """Return Dataset's spatial info.""" - return SpatialInfo( - bounds=self.bounds, - center=self.center, - minzoom=self.minzoom, - maxzoom=self.maxzoom, - ) + def geographic_bounds(self) -> BBox: + """return bounds in WGS84.""" + try: + bounds = transform_bounds( + self.crs, WGS84_CRS, *self.bounds, densify_pts=21, + ) + except: # noqa + warnings.warn( + "Cannot dertermine bounds in WGS84, will default to (-180.0, -90.0, 180.0, 90.0).", + UserWarning, + ) + bounds = (-180.0, -90, 180.0, 90) + + return bounds def tile_exists(self, tile_x: int, tile_y: int, tile_z: int) -> bool: """Check if a tile intersects the dataset bounds. @@ -69,13 +71,25 @@ def tile_exists(self, tile_x: int, tile_y: int, tile_z: int) -> bool: bool: True if the tile intersects the dataset bounds. """ - tile = Tile(x=tile_x, y=tile_y, z=tile_z) - tile_bounds = self.tms.bounds(tile) + tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) + + try: + dataset_bounds = transform_bounds( + self.crs, self.tms.rasterio_crs, *self.bounds, densify_pts=21, + ) + except: # noqa + # HACK: gdal will first throw an error for invalid transformation + # but if retried it will then pass. + # Note: It might return `+/-inf` values + dataset_bounds = transform_bounds( + self.crs, self.tms.rasterio_crs, *self.bounds, densify_pts=21, + ) + return ( - (tile_bounds[0] < self.bounds[2]) - and (tile_bounds[2] > self.bounds[0]) - and (tile_bounds[3] > self.bounds[1]) - and (tile_bounds[1] < self.bounds[3]) + (tile_bounds[0] < dataset_bounds[2]) + and (tile_bounds[2] > dataset_bounds[0]) + and (tile_bounds[3] > dataset_bounds[1]) + and (tile_bounds[1] < dataset_bounds[3]) ) @@ -352,6 +366,7 @@ class MultiBaseReader(BaseReader, metaclass=abc.ABCMeta): reader: Type[BaseReader] = attr.ib() reader_options: Dict = attr.ib(factory=dict) + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) assets: Sequence[str] = attr.ib(init=False) @@ -766,6 +781,7 @@ class MultiBandReader(BaseReader, metaclass=abc.ABCMeta): reader: Type[BaseReader] = attr.ib() reader_options: Dict = attr.ib(factory=dict) + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) bands: Sequence[str] = attr.ib(init=False) @@ -806,7 +822,11 @@ def _reader(band: str, **kwargs: Any) -> Info: bands_metadata = multi_values(bands, _reader, *args, **kwargs) - meta = self.spatial_info.dict() + meta = { + "bounds": self.geographic_bounds, + "minzoom": self.minzoom, + "maxzoom": self.maxzoom, + } # We only keep the value for the first band. meta["band_metadata"] = [ @@ -894,7 +914,11 @@ def _reader(band: str, *args, **kwargs) -> Metadata: bands_metadata = multi_values(bands, _reader, pmin, pmax, **kwargs) - meta = self.spatial_info.dict() + meta = { + "bounds": self.geographic_bounds, + "minzoom": self.minzoom, + "maxzoom": self.maxzoom, + } meta["band_metadata"] = [ (band, bands_metadata[band].band_metadata[0][1]) for ix, band in enumerate(bands) diff --git a/rio_tiler/io/cogeo.py b/rio_tiler/io/cogeo.py index b40a28e3..29d8e130 100644 --- a/rio_tiler/io/cogeo.py +++ b/rio_tiler/io/cogeo.py @@ -33,8 +33,8 @@ class COGReader(BaseReader): filepath (str): Cloud Optimized GeoTIFF 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`. - minzoom (int, optional): Overwrite Min Zoom level. - maxzoom (int, optional): Overwrite Max Zoom level. + minzoom (int, optional): Set minzoom for the tiles. + maxzoom (int, optional): Set maxzoom for the tiles. colormap (dict, optional): Overwrite internal colormap. nodata (int or float or str, optional): Global options, overwrite internal nodata value. unscale (bool, optional): Global options, apply internal scale and offset on all read operations. @@ -65,9 +65,11 @@ class COGReader(BaseReader): dataset: Union[DatasetReader, DatasetWriter, MemoryFile, WarpedVRT] = attr.ib( default=None ) + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) minzoom: int = attr.ib(default=None) maxzoom: int = attr.ib(default=None) + colormap: Dict = attr.ib(default=None) # Define global options to be forwarded to functions reading the data (e.g `rio_tiler.reader.read`) @@ -97,12 +99,11 @@ def __attrs_post_init__(self): self._kwargs["post_process"] = self.post_process self.dataset = self.dataset or rasterio.open(self.filepath) + self.bounds = tuple(self.dataset.bounds) + self.crs = self.dataset.crs self.nodata = self.nodata if self.nodata is not None else self.dataset.nodata - self.bounds = transform_bounds( - self.dataset.crs, WGS84_CRS, *self.dataset.bounds, densify_pts=21 - ) if self.minzoom is None or self.maxzoom is None: self._set_zooms() @@ -127,7 +128,7 @@ def __exit__(self, exc_type, exc_value, traceback): self.close() def get_zooms(self, tilesize: int = 256) -> Tuple[int, int]: - """Calculate raster min/max zoom level.""" + """Calculate raster min/max zoom level for input TMS.""" if self.dataset.crs != self.tms.rasterio_crs: dst_affine, w, h = calculate_default_transform( self.dataset.crs, @@ -141,18 +142,30 @@ def get_zooms(self, tilesize: int = 256) -> Tuple[int, int]: w = self.dataset.width h = self.dataset.height + # The maxzoom is defined by finding the minimum difference between + # the raster resolution and the zoom level resolution resolution = max(abs(dst_affine[0]), abs(dst_affine[4])) maxzoom = self.tms.zoom_for_res(resolution) + # The minzoom is defined by the resolution of the maximum theoretical overview level overview_level = get_maximum_overview_level(w, h, minsize=tilesize) ovr_resolution = resolution * (2 ** overview_level) minzoom = self.tms.zoom_for_res(ovr_resolution) - return minzoom, maxzoom + return (minzoom, maxzoom) def _set_zooms(self): """Calculate raster min/max zoom level.""" - minzoom, maxzoom = self.get_zooms() + try: + minzoom, maxzoom = self.get_zooms() + except: # noqa + # if we can't get min/max zoom from the dataset we default to TMS min/max zoom + warnings.warn( + "Cannot dertermine min/max zoom based on dataset informations, will default to TMS min/max zoom.", + UserWarning, + ) + minzoom, maxzoom = self.tms.minzoom, self.tms.maxzoom + self.minzoom = self.minzoom if self.minzoom is not None else minzoom self.maxzoom = self.maxzoom if self.maxzoom is not None else maxzoom return @@ -182,8 +195,7 @@ def _get_descr(ix): nodata_type = "None" meta = { - "bounds": self.bounds, - "center": self.center, + "bounds": self.geographic_bounds, "minzoom": self.minzoom, "maxzoom": self.maxzoom, "band_metadata": [ @@ -425,17 +437,14 @@ def preview( data = apply_expression(blocks, bands, data) return ImageData( - data, - mask, - bounds=self.dataset.bounds, - crs=self.dataset.crs, - assets=[self.filepath], + data, mask, bounds=self.bounds, crs=self.crs, assets=[self.filepath], ) def point( self, lon: float, lat: float, + coord_crs: CRS = WGS84_CRS, indexes: Optional[Indexes] = None, expression: Optional[str] = None, **kwargs: Any, @@ -445,6 +454,7 @@ def point( Args: lon (float): Longitude. lat (float): Latittude. + coord_crs (rasterio.crs.CRS, optional): Coordinate Reference System of the input coords. Defaults to `epsg:4326`. indexes (sequence of int or int, optional): Band indexes. expression (str, optional): rio-tiler expression (e.g. b1/b2+b3). kwargs (optional): Options to forward to the `rio_tiler.reader.point` function. @@ -467,7 +477,9 @@ def point( if expression: indexes = parse_expression(expression) - point = reader.point(self.dataset, (lon, lat), indexes=indexes, **kwargs) + point = reader.point( + self.dataset, (lon, lat), indexes=indexes, coord_crs=coord_crs, **kwargs + ) if expression: blocks = expression.lower().split(",") @@ -567,11 +579,7 @@ def read( data = apply_expression(blocks, bands, data) return ImageData( - data, - mask, - bounds=self.dataset.bounds, - crs=self.dataset.crs, - assets=[self.filepath], + data, mask, bounds=self.bounds, crs=self.crs, assets=[self.filepath], ) diff --git a/rio_tiler/io/stac.py b/rio_tiler/io/stac.py index c5d06fea..6555d60c 100644 --- a/rio_tiler/io/stac.py +++ b/rio_tiler/io/stac.py @@ -10,7 +10,7 @@ import requests from morecantile import TileMatrixSet -from ..constants import WEB_MERCATOR_TMS +from ..constants import WEB_MERCATOR_TMS, WGS84_CRS from ..errors import InvalidAssetName, MissingAssets from ..utils import aws_get_object from .base import BaseReader, MultiBaseReader @@ -125,7 +125,7 @@ class STACReader(MultiBaseReader): filepath (str): STAC Item path, URL or S3 URL. item (dict or pystac.Item, STAC): Stac Item. minzoom (int, optional): Set minzoom for the tiles. - minzoom (int, optional): Set maxzoom for the tiles. + maxzoom (int, optional): Set maxzoom for the tiles. include (set of string, optional): Only Include specific assets. exclude (set of string, optional): Exclude specific assets. include_asset_types (set of string, optional): Only include some assets base on their type. @@ -155,15 +155,20 @@ class STACReader(MultiBaseReader): filepath: 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) + include_assets: Optional[Set[str]] = attr.ib(default=None) exclude_assets: Optional[Set[str]] = attr.ib(default=None) + include_asset_types: Set[str] = attr.ib(default=DEFAULT_VALID_TYPE) exclude_asset_types: Optional[Set[str]] = attr.ib(default=None) + reader: Type[BaseReader] = attr.ib(default=COGReader) reader_options: Dict = attr.ib(factory=dict) + fetch_options: Dict = attr.ib(factory=dict) def __attrs_post_init__(self): @@ -171,7 +176,11 @@ def __attrs_post_init__(self): self.item = self.item or pystac.Item.from_dict( fetch(self.filepath, **self.fetch_options), self.filepath ) + + # TODO: get bounds/crs using PROJ extension if availble self.bounds = self.item.bbox + self.crs = WGS84_CRS + self.assets = list( _get_assets( self.item, @@ -185,9 +194,11 @@ def __attrs_post_init__(self): raise MissingAssets("No valid asset found") if self.minzoom is None: + # TODO get minzoom from PROJ extension self.minzoom = self.tms.minzoom if self.maxzoom is None: + # TODO get maxzoom from PROJ extension self.maxzoom = self.tms.maxzoom def _get_asset_url(self, asset: str) -> str: diff --git a/rio_tiler/models.py b/rio_tiler/models.py index b92f0ef5..b1b14fff 100644 --- a/rio_tiler/models.py +++ b/rio_tiler/models.py @@ -48,7 +48,6 @@ class Bounds(RioTilerBaseModel): class SpatialInfo(Bounds): """Dataset SpatialInfo""" - center: Tuple[NumType, NumType, int] minzoom: int maxzoom: int diff --git a/tests/fixtures/cog_nonearth.tif b/tests/fixtures/cog_nonearth.tif new file mode 100644 index 00000000..58678a66 Binary files /dev/null and b/tests/fixtures/cog_nonearth.tif differ diff --git a/tests/test_io_MultiBand.py b/tests/test_io_MultiBand.py index 0e823c2c..8eb4ac59 100644 --- a/tests/test_io_MultiBand.py +++ b/tests/test_io_MultiBand.py @@ -23,6 +23,7 @@ class BandFileReader(MultiBandReader): path: str = attr.ib() reader: Type[BaseReader] = attr.ib(default=COGReader) reader_options: Dict = attr.ib(factory=dict) + tms: morecantile.TileMatrixSet = attr.ib(default=default_tms) def __attrs_post_init__(self): @@ -32,6 +33,7 @@ def __attrs_post_init__(self): ) with self.reader(self._get_band_url(self.bands[0])) as cog: self.bounds = cog.bounds + self.crs = cog.crs self.minzoom = cog.minzoom self.maxzoom = cog.maxzoom @@ -44,7 +46,11 @@ def test_MultiBandReader(): """Should work as expected.""" with BandFileReader(PREFIX) as cog: assert cog.bands == ["b1", "b2"] - assert cog.spatial_info + assert cog.minzoom is not None + assert cog.maxzoom is not None + assert cog.bounds + assert cog.bounds + assert cog.crs assert sorted(cog.parse_expression("b1/b2")) == ["b1", "b2"] diff --git a/tests/test_io_async.py b/tests/test_io_async.py index 26ffe88f..6b2e676a 100644 --- a/tests/test_io_async.py +++ b/tests/test_io_async.py @@ -54,6 +54,7 @@ class AsyncCOGReader(AsyncBaseReader): def __attrs_post_init__(self): """Update dataset info.""" self.bounds = self.dataset.bounds + self.crs = self.dataset.crs self.minzoom = self.dataset.minzoom self.maxzoom = self.dataset.maxzoom @@ -104,9 +105,8 @@ async def test_async(): info = await cog.info() assert info == dataset.info() - meta = cog.spatial_info - assert meta.minzoom == 5 - assert meta.maxzoom == 9 + assert cog.minzoom == 5 + assert cog.maxzoom == 9 assert await cog.stats(5, 95) with pytest.warns(DeprecationWarning): diff --git a/tests/test_io_cogeo.py b/tests/test_io_cogeo.py index 1a8f5dc8..17f22830 100644 --- a/tests/test_io_cogeo.py +++ b/tests/test_io_cogeo.py @@ -7,8 +7,11 @@ import numpy import pytest import rasterio +from morecantile import TileMatrixSet +from pyproj import CRS from rasterio.io import DatasetReader, MemoryFile from rasterio.vrt import WarpedVRT +from rasterio.warp import transform_bounds from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS from rio_tiler.errors import ( @@ -31,6 +34,7 @@ COG_DLINE = os.path.join(PREFIX, "cog_dateline.tif") COG_EARTH = os.path.join(PREFIX, "cog_fullearth.tif") GEOTIFF = os.path.join(PREFIX, "nocog.tif") +COG_EUROPA = os.path.join(PREFIX, "cog_nonearth.tif") KEY_ALPHA = "hro_sources/colorado/201404_13SED190110_201404_0x1500m_CL_1_alpha.tif" COG_ALPHA = os.path.join(PREFIX, "my-bucket", KEY_ALPHA) @@ -46,10 +50,10 @@ def test_spatial_info_valid(): """Should work as expected (get spatial info)""" with COGReader(COG_NODATA) as cog: assert not cog.dataset.closed - meta = cog.spatial_info - assert meta["minzoom"] == 5 - assert meta.minzoom == 5 - assert meta.maxzoom == 9 + assert cog.bounds + assert cog.crs + assert cog.minzoom == 5 + assert cog.maxzoom == 9 assert cog.nodata == cog.dataset.nodata assert cog.dataset.closed @@ -59,19 +63,16 @@ def test_spatial_info_valid(): assert cog.dataset.closed with COGReader(COG_NODATA, minzoom=3) as cog: - meta = cog.spatial_info - assert meta.minzoom == 3 - assert meta.maxzoom == 9 + assert cog.minzoom == 3 + assert cog.maxzoom == 9 with COGReader(COG_NODATA, maxzoom=12) as cog: - meta = cog.spatial_info - assert meta.minzoom == 5 - assert meta.maxzoom == 12 + assert cog.minzoom == 5 + assert cog.maxzoom == 12 with COGReader(COG_NODATA, minzoom=3, maxzoom=12) as cog: - meta = cog.spatial_info - assert meta.minzoom == 3 - assert meta.maxzoom == 12 + assert cog.minzoom == 3 + assert cog.maxzoom == 12 def test_bounds_valid(): @@ -719,3 +720,52 @@ def test_no_overviews(): with pytest.warns(NoOverviewWarning): with COGReader(GEOTIFF): pass + + +def test_nonearthbody(): + """COGReader should work with non-earth dataset.""" + with pytest.warns(UserWarning): + with COGReader(COG_EUROPA) as cog: + assert cog.minzoom == 0 + assert cog.maxzoom == 24 + + with pytest.warns(None) as warnings: + with COGReader(COG_EUROPA) as cog: + assert cog.info() + assert len(warnings) == 2 + + img = cog.read() + assert numpy.array_equal(img.data, cog.dataset.read(indexes=(1,))) + assert img.width == cog.dataset.width + assert img.height == cog.dataset.height + assert img.count == cog.dataset.count + + img = cog.preview() + assert img.bounds == cog.bounds + + part = cog.part(cog.bounds, bounds_crs=cog.crs) + assert part.bounds == cog.bounds + + lon = (cog.bounds[0] + cog.bounds[2]) / 2 + lat = (cog.bounds[1] + cog.bounds[3]) / 2 + assert cog.point(lon, lat, coord_crs=cog.crs)[0] is not None + + 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 pytest.warns(None) as warnings: + with COGReader(COG_EUROPA, tms=tms) as cog: + assert cog.minzoom == 4 + assert cog.maxzoom == 6 + + # Get Tile covering the UL corner + bounds = transform_bounds(cog.crs, tms.rasterio_crs, *cog.bounds) + t = tms._tile(bounds[0], bounds[1], cog.minzoom) + img = cog.tile(t.x, t.y, t.z) + + assert img.height == 256 + assert img.width == 256 + assert img.crs == tms.rasterio_crs + + assert len(warnings) == 0 diff --git a/tests/test_io_stac.py b/tests/test_io_stac.py index ba648034..f04acdd6 100644 --- a/tests/test_io_stac.py +++ b/tests/test_io_stac.py @@ -38,8 +38,6 @@ def test_fetch_stac(requests, s3_get): assert stac.minzoom == 0 assert stac.maxzoom == 24 assert stac.bounds - assert stac.center - assert stac.spatial_info assert stac.filepath == STAC_PATH assert stac.assets == ["red", "green", "blue"] requests.assert_not_called()