diff --git a/tests/fixtures/catalog.json b/tests/fixtures/catalog.json index 4f2cb24..292a6e6 100644 --- a/tests/fixtures/catalog.json +++ b/tests/fixtures/catalog.json @@ -153,6 +153,157 @@ "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", "https://stac-extensions.github.io/render/v1.0.0/schema.json" ] + }, + { + "id": "MAXAR_BayofBengal_Cyclone_Mocha_May_23_cube_dimensions", + "type": "Collection", + "links": [ + { + "rel": "items", + "type": "application/geo+json", + "href": "https://stac.endpoint.io/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23/items" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.endpoint.io/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23" + } + ], + "title": "Bay of Bengal Cyclone Mocha 2023", + "cube:dimensions": { + "time": { + "type": "temporal", + "extent": [ + "2023-01-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ], + "values": [ + "2023-01-03T04:30:17Z", + "2023-02-03T04:30:17Z", + "2023-03-03T04:30:17Z", + "2023-04-03T04:30:17Z", + "2023-05-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ] + } + }, + "extent": { + "spatial": { + "bbox": [ + [ + 91.831615, + 19.982078842323997, + 92.97426268500965, + 21.666101 + ], + [ + 92.567815, + 20.18811887678192, + 92.74417544237298, + 20.62968532404085 + ], + [ + 92.72278776887262, + 20.104801, + 92.893524, + 20.630214 + ], + [ + 92.75855246040959, + 19.982078842323997, + 92.89682495377032, + 20.514473160464657 + ], + [ + 92.84253515935835, + 19.984656587012033, + 92.97426268500965, + 20.514418665444474 + ], + [ + 91.831615, + 21.518411, + 91.957078, + 21.666101 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2023-01-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ] + ] + } + }, + "license": "CC-BY-NC-4.0", + "renders": { + "visual": { + "title": "Visual Image", + "assets": [ + "visual" + ], + "asset_bidx": "visual|1,2,3", + "minmax_zoom": [ + 8, + 22 + ], + "tilematrixsets": { + "WebMercatorQuad": [ + 8, + 22 + ] + } + } + }, + "description": "Maxar OpenData | Cyclone Mocha, a category five cyclone with 130 mph winds and torrential rain, hit parts of Myanmar and Bangladesh, forcing mass evacuations ahead of the storm. The cyclone, one of the most powerful to hit the region in the last decade, made landfall on Sunday, May 14, 2023, near Sittwe in Myanmar's Rakhine state. Rain and a storm surge caused widespread flooding in low-lying areas. The United National Office Coordination of Humanitarian Affairs stated that there had been extensive damage among already vulnerable communities and that communications with the affected areas have been difficult.", + "item_assets": { + "visual": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "visual" + ], + "title": "Visual Image" + }, + "data-mask": { + "type": "application/geopackage+sqlite3", + "roles": [ + "data-mask" + ], + "title": "Data Mask" + }, + "ms_analytic": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Multispectral Image" + }, + "pan_analytic": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Panchromatic Image" + } + }, + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v1.0.0/schema.json", + "https://stac-extensions.github.io/datacube/v2.2.0/schema.json" + ] } ], "links": [ diff --git a/tests/test_render.py b/tests/test_render.py index 379962d..25c1c33 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -25,7 +25,7 @@ def test_render(client): collections_render = get_layer_from_collections( "https://something.stac", None, None ) - assert len(collections_render) == 3 + assert len(collections_render) == 4 visual = collections_render["MAXAR_BayofBengal_Cyclone_Mocha_May_23_visual"] assert visual["bbox"] diff --git a/tests/test_wmts.py b/tests/test_wmts.py index ffb3220..ecee9de 100644 --- a/tests/test_wmts.py +++ b/tests/test_wmts.py @@ -88,10 +88,11 @@ def test_wmts_getcapabilities(client, app): assert response.status_code == 200 wmts = WebMapTileService(url="/wmts", xml=response.text.encode()) layers = list(wmts.contents) - assert len(layers) == 3 + assert len(layers) == 4 assert "MAXAR_BayofBengal_Cyclone_Mocha_May_23_visual" in layers assert "MAXAR_BayofBengal_Cyclone_Mocha_May_23_color" in layers assert "MAXAR_BayofBengal_Cyclone_Mocha_May_23_visualr" in layers + assert "MAXAR_BayofBengal_Cyclone_Mocha_May_23_cube_dimensions_visual" in layers layer = wmts["MAXAR_BayofBengal_Cyclone_Mocha_May_23_visual"] assert "WebMercatorQuad" in layer.tilematrixsetlinks @@ -104,6 +105,11 @@ def test_wmts_getcapabilities(client, app): assert query["assets"] == ["visual"] assert query["asset_bidx"] == ["visual|1,2,3"] + layer = wmts["MAXAR_BayofBengal_Cyclone_Mocha_May_23_cube_dimensions_visual"] + assert "TIME" in layer.dimensions + times = layer.dimensions["TIME"]["values"] + assert len(times) == 6 + @patch("rio_tiler.io.rasterio.rasterio") @patch("titiler.stacapi.factory.STACAPIBackend.get_assets") @@ -287,6 +293,63 @@ def test_wmts_gettile(client, get_assets, rio, app): assert response.status_code == 200 +@patch("rio_tiler.io.rasterio.rasterio") +@patch("titiler.stacapi.factory.STACAPIBackend.get_assets") +@patch("titiler.stacapi.factory.Client") +def test_wmts_gettile_param_override(client, get_assets, rio, app): + """test STAC items endpoints.""" + rio.open = mock_rasterio_open + + with open(catalog_json, "r") as f: + collections = [ + pystac.Collection.from_dict(c) for c in json.loads(f.read())["collections"] + ] + client.open.return_value.get_collections.return_value = collections + + with open(item_json, "r") as f: + get_assets.return_value = [json.loads(f.read())] + + response = app.get( + "/wmts", + params={ + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "getTile", + "LAYER": "MAXAR_BayofBengal_Cyclone_Mocha_May_23_visual", + "STYLE": "default", + "FORMAT": "image/png", + "TILEMATRIXSET": "WebMercatorQuad", + "TILEMATRIX": 14, + "TILEROW": 7188, + "TILECOL": 12375, + "TIME": "2023-01-05", + "expression": "(where(visual_invalid >= 0))", + }, + ) + assert response.status_code == 500 + assert "Could not find any valid assets" in response.json()["detail"] + + response = app.get( + "/wmts", + params={ + "SERVICE": "WMTS", + "VERSION": "1.0.0", + "REQUEST": "getTile", + "LAYER": "MAXAR_BayofBengal_Cyclone_Mocha_May_23_color", + "STYLE": "default", + "FORMAT": "image/png", + "TILEMATRIXSET": "WebMercatorQuad", + "TILEMATRIX": 14, + "TILEROW": 7188, + "TILECOL": 12375, + "TIME": "2023-01-05", + "colormap": "{invalid}", + }, + ) + assert response.status_code == 400 + assert "Could not parse the colormap value" in response.json()["detail"] + + @patch("rio_tiler.io.rasterio.rasterio") @patch("titiler.stacapi.factory.STACAPIBackend.get_assets") @patch("titiler.stacapi.factory.Client") diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index f3767fd..854c25f 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -3,6 +3,7 @@ import datetime as python_datetime import json import os +from copy import copy from dataclasses import dataclass, field from enum import Enum from typing import Any, Callable, Dict, List, Literal, Optional, Type @@ -629,10 +630,20 @@ def get_layer_from_collections( # noqa: C901 tms_id: None for tms_id in tilematrixsets } - # TODO: handle multiple intervals - # Check datacube extension - # https://github.com/stac-extensions/datacube?tab=readme-ov-file#temporal-dimension-object - if intervals := temporal_extent.intervals: + if ( + "cube:dimensions" in collection.extra_fields + and "time" in collection.extra_fields["cube:dimensions"] + ): + layer["time"] = [ + python_datetime.datetime.strptime( + t, + "%Y-%m-%dT%H:%M:%SZ", + ).strftime("%Y-%m-%d") + for t in collection.extra_fields["cube:dimensions"]["time"][ + "values" + ] + ] + elif intervals := temporal_extent.intervals: start_date = intervals[0][0] end_date = ( intervals[0][1] @@ -792,7 +803,12 @@ def get_tile( # noqa: C901 "datetime" ] = f"{start_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')}/{end_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')}" - query_params = layer.get("render") or {} + query_params = copy(layer.get("render")) or {} + if "color_formula" in req: + query_params["color_formula"] = req["color_formula"] + if "expression" in req: + query_params["expression"] = req["expression"] + layer_params = get_dependency_params( dependency=self.layer_dependency, query_params=query_params, @@ -959,6 +975,26 @@ def register_routes(self): # noqa: C901 "name": "TileCol", "in": "query", }, + { + "required": False, + "schema": { + "title": "Color Formula", + "description": "rio-color formula (info: https://github.com/mapbox/rio-color)", + "type": "string", + }, + "name": "color_formula", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Colormap name", + "description": "JSON encoded custom Colormap", + "type": "string", + }, + "name": "colormap", + "in": "query", + }, ################ # GetFeatureInfo # InfoFormat @@ -1126,7 +1162,9 @@ def web_map_tile_service( # noqa: C901 colormap = get_dependency_params( dependency=self.colormap_dependency, - query_params=layer.get("render") or {}, + query_params={"colormap": req["colormap"]} + if "colormap" in req + else layer.get("render") or {}, ) content, media_type = render_image( diff --git a/titiler/stacapi/templates/wmts-getcapabilities_1.0.0.xml b/titiler/stacapi/templates/wmts-getcapabilities_1.0.0.xml index e896e65..9bc6bec 100644 --- a/titiler/stacapi/templates/wmts-getcapabilities_1.0.0.xml +++ b/titiler/stacapi/templates/wmts-getcapabilities_1.0.0.xml @@ -114,7 +114,7 @@ {{ tms.id }} {% if tms.crs.to_epsg() %} - urn:ogc:def:crs:epsg::{{tms.crs.to_epsg()}} + urn:ogc:def:crs:EPSG::{{tms.crs.to_epsg()}} {% else %} {{ tms.crs.srs.replace("http://www.opengis.net/def/", "urn:ogc:def:").replace("/", ":")}} {% endif %}