From 84c6f7b9ddeef94604b80626a35b32bacb44a793 Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Fri, 3 May 2024 13:45:46 +0200 Subject: [PATCH 1/8] feat: added support for time retrieval from cube dimensions STAC extension --- titiler/stacapi/factory.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index 8aa367c..6a933e2 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -629,10 +629,10 @@ def get_layer_from_collections( 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"] = collection.extra_fields["cube:dimensions"]["time"]["values"] + elif intervals := temporal_extent.intervals: start_date = intervals[0][0] end_date = ( intervals[0][1] From cd26d4eb616a50b84225c6eddba23d373015df18 Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Fri, 3 May 2024 13:46:25 +0200 Subject: [PATCH 2/8] fix: supportedCRS uppercase EPSG identifier --- titiler/stacapi/templates/wmts-getcapabilities_1.0.0.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 %} From 0a50f03c0c8e35da3eba8c9d7c774ea72c950114 Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Fri, 3 May 2024 14:18:14 +0200 Subject: [PATCH 3/8] fix: remove time information from cube:dimensions dates in capabilities --- titiler/stacapi/factory.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index 6a933e2..73b6054 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -631,7 +631,13 @@ def get_layer_from_collections( if ("cube:dimensions" in collection.extra_fields and "time" in collection.extra_fields["cube:dimensions"]): - layer["time"] = collection.extra_fields["cube:dimensions"]["time"]["values"] + 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 = ( From 49f0f6ac8e414af26072baa34cf294093ca1d145 Mon Sep 17 00:00:00 2001 From: Stijn Caerts Date: Tue, 7 May 2024 11:54:42 +0200 Subject: [PATCH 4/8] fix encoding of colormap configured in STAC collection --- titiler/stacapi/factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index 73b6054..f8751af 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -656,6 +656,8 @@ def get_layer_from_collections( # TODO: # special encoding for ColorMaps render = layer["render"] or {} + if "colormap" in render: + render["colormap"] = json.dumps(render["colormap"]) qs = urlencode( [(k, v) for k, v in render.items() if v is not None], doseq=True, From 47917b23ee8336d8d709a785e3dc3b908df43278 Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Wed, 12 Jun 2024 13:56:05 +0200 Subject: [PATCH 5/8] feat: allow overriding of color_formula, expression and colormap from url params --- titiler/stacapi/factory.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index f8751af..5e78239 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -781,6 +781,11 @@ def get_tile( # noqa: C901 ] = 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 {} + 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, @@ -955,6 +960,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 @@ -1122,7 +1147,7 @@ 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( From 418b25a1f1ac7bac57d25d1ce5974bc9c6142f2a Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Wed, 12 Jun 2024 14:35:06 +0200 Subject: [PATCH 6/8] fix: python 3.12 code formatting --- titiler/stacapi/factory.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index 4e8b348..51fde9b 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -629,14 +629,18 @@ def get_layer_from_collections( # noqa: C901 tms_id: None for tms_id in tilematrixsets } - if ("cube:dimensions" in collection.extra_fields - and "time" in collection.extra_fields["cube:dimensions"]): + 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"] + for t in collection.extra_fields["cube:dimensions"]["time"][ + "values" + ] ] elif intervals := temporal_extent.intervals: start_date = intervals[0][0] @@ -1157,7 +1161,9 @@ def web_map_tile_service( # noqa: C901 colormap = get_dependency_params( dependency=self.colormap_dependency, - query_params={"colormap": req["colormap"]} if "colormap" in req else layer.get("render") or {}, + query_params={"colormap": req["colormap"]} + if "colormap" in req + else layer.get("render") or {}, ) content, media_type = render_image( From c15c90877c9d519ab1d69f0ddcdebefcb7f183fc Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Mon, 12 Aug 2024 17:37:13 +0200 Subject: [PATCH 7/8] chore: added additional tests for cube:dimensions extension and WMTS parameter override --- tests/fixtures/catalog.json | 151 ++++++++++++++++++++++++++++++++++++ tests/test_render.py | 2 +- tests/test_wmts.py | 65 +++++++++++++++- 3 files changed, 216 insertions(+), 2 deletions(-) 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") From 216e9305460fb7b63bf97ae70fbbb24b96da3357 Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Mon, 12 Aug 2024 17:59:28 +0200 Subject: [PATCH 8/8] fix: remove reference to layer render config to not interfere with others --- titiler/stacapi/factory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index 51fde9b..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 @@ -802,7 +803,7 @@ 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: