diff --git a/CHANGES.md b/CHANGES.md index ef70aef32..1f5025891 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Release Notes +## 0.3.5 (TBD) + +### titiler.mosaic + +* add `/{quadkey}/assets`, `/{lon},{lat}/assets`, `/{minx},{miny},{maxx},{maxy}/assets` GET endpoints to return a list of assets that intersect a given geometry (author @mackdelany, https://github.com/developmentseed/titiler/pull/351) + ## 0.3.4 (2021-08-02) ### titiler.core diff --git a/docs/endpoints/mosaic.md b/docs/endpoints/mosaic.md index a3fd145b5..fe8873a42 100644 --- a/docs/endpoints/mosaic.md +++ b/docs/endpoints/mosaic.md @@ -17,6 +17,9 @@ Read Mosaic Info/Metadata and create Web map Tiles from a multiple COG. The `mos | `GET` | `/mosaicjson/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/mosaicjson/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/mosaicjson/point/{lon},{lat}` | JSON | return pixel value from a MosaicJSON dataset +| `GET` | `/mosaicjson/{quadkey}/assets` | JSON | return list of assets intersecting a quadkey +| `GET` | `/mosaicjson/{lon},{lat}/assets` | JSON | return list of assets intersecting a point +| `GET` | `/mosaicjson/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box ## Description diff --git a/src/titiler/mosaic/setup.py b/src/titiler/mosaic/setup.py index 0f6cefab9..2098fe580 100644 --- a/src/titiler/mosaic/setup.py +++ b/src/titiler/mosaic/setup.py @@ -5,10 +5,7 @@ with open("README.md") as f: long_description = f.read() -inst_reqs = [ - "titiler.core", - "cogeo-mosaic>=3.0,<3.1", -] +inst_reqs = ["titiler.core", "cogeo-mosaic>=3.0,<3.1", "mercantile"] extra_reqs = { "test": ["pytest", "pytest-cov", "pytest-asyncio", "requests"], } diff --git a/src/titiler/mosaic/tests/test_factory.py b/src/titiler/mosaic/tests/test_factory.py index af2001b9b..20c1f7342 100644 --- a/src/titiler/mosaic/tests/test_factory.py +++ b/src/titiler/mosaic/tests/test_factory.py @@ -41,7 +41,7 @@ def test_MosaicTilerFactory(): optional_headers=[OptionalHeader.server_timing, OptionalHeader.x_assets], router_prefix="mosaic", ) - assert len(mosaic.router.routes) == 19 + assert len(mosaic.router.routes) == 22 assert mosaic.tms_dependency == WebMercatorTMSParams app = FastAPI() @@ -142,3 +142,32 @@ def test_MosaicTilerFactory(): "/mosaic/validate", json=MosaicJSON.from_urls(assets).dict(), ) assert response.status_code == 200 + + response = client.get("/mosaic/0302302/assets", params={"url": mosaic_file},) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif"] for filepath in response.json() + ) + + response = client.get("/mosaic/-71,46/assets", params={"url": mosaic_file}) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + + response = client.get( + "/mosaic/-75.9375,43.06888777416962,-73.125,45.089035564831015/assets", + params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert all( + filepath.split("/")[-1] in ["cog1.tif", "cog2.tif"] + for filepath in response.json() + ) + + response = client.get( + "/mosaic/10,10,11,11/assets", params={"url": mosaic_file}, + ) + assert response.status_code == 200 + assert response.json() == [] diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index 536961782..602e5a872 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -5,6 +5,7 @@ from typing import Callable, Dict, Optional, Type from urllib.parse import urlencode, urlparse +import mercantile import rasterio from cogeo_mosaic.backends import BaseBackend, MosaicBackend from cogeo_mosaic.models import Info as mosaicInfo @@ -64,6 +65,7 @@ def register_routes(self): self.wmts() self.point() self.validate() + self.assets() ############################################################################ # /read @@ -484,3 +486,65 @@ def validate(self): def validate(body: MosaicJSON): """Validate a MosaicJSON""" return True + + def assets(self): + """Register /assets endpoint.""" + + @self.router.get( + r"/{minx},{miny},{maxx},{maxy}/assets", + responses={200: {"description": "Return list of COGs in bounding box"}}, + ) + def bbox( + src_path=Depends(self.path_dependency), + minx: float = Query(None, description="Left side of bounding box"), + miny: float = Query(None, description="Bottom of bounding box"), + maxx: float = Query(None, description="Right side of bounding box"), + maxy: float = Query(None, description="Top of bounding box"), + ): + """Return a list of assets which overlap a bounding box""" + with self.reader(src_path, **self.backend_options) as mosaic: + tl_tile = mercantile.tile(minx, maxy, mosaic.minzoom) + br_tile = mercantile.tile(maxx, miny, mosaic.minzoom) + tiles = [ + (x, y, mosaic.minzoom) + for x in range(tl_tile.x, br_tile.x + 1) + for y in range(tl_tile.y, br_tile.y + 1) + ] + assets = list( + { + asset + for asset_list in [mosaic.assets_for_tile(*t) for t in tiles] + for asset in asset_list + } + ) + + return assets + + @self.router.get( + r"/{lng},{lat}/assets", + responses={200: {"description": "Return list of COGs"}}, + ) + def lonlat( + src_path=Depends(self.path_dependency), + lng: float = Query(None, description="Longitude"), + lat: float = Query(None, description="Latitude"), + ): + """Return a list of assets which overlap a point""" + with self.reader(src_path, **self.backend_options) as mosaic: + assets = mosaic.assets_for_point(lng, lat) + + return assets + + @self.router.get( + r"/{quadkey}/assets", + responses={200: {"description": "Return list of COGs"}}, + ) + def quadkey( + src_path=Depends(self.path_dependency), + quadkey: str = Query(None, description="Quadkey to return COGS for."), + ): + """Return a list of assets which overlap a given quadkey""" + with self.reader(src_path, **self.backend_options) as mosaic: + assets = mosaic.assets_for_tile(*mercantile.quadkey_to_tile(quadkey)) + + return assets