diff --git a/CHANGES.md b/CHANGES.md index 4fdd56a2c..9265882d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ ### Misc -* Removed default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints **breaking change** +* Remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints **breaking change** ``` # Before @@ -23,9 +23,14 @@ * Use `@attrs.define` instead of dataclass for factories **breaking change** * Use `@attrs.define` instead of dataclass for factory extensions **breaking change** - ### titiler.core +* Update `rio-tiler` dependency to `>=7.0,<8.0` + +* Update `geojson-pydantic` dependency to `>=1.1.2,<2.0` which better handle antimeridian crossing dataset + +* handle `antimeridian` crossing bounds in `/info.geojson` endpoints (returning MultiPolygon instead of Polygon) + * Improve XSS security for HTML templates (author @jcary741, https://github.com/developmentseed/titiler/pull/953) * Remove all default values to the dependencies **breaking change** @@ -57,19 +62,29 @@ >> {'value': 1} ``` -* fix Hillshade algorithm (bad `azimuth` angle) +* Fix Hillshade algorithm (bad `azimuth` angle) -* set default `azimuth` and `altitude` angles to 45º for the Hillshade algorithm **breaking change** +* Set default `azimuth` and `altitude` angles to 45º for the Hillshade algorithm **breaking change** * Use `.as_dict()` method when passing option to rio-tiler Reader's methods to avoid parameter conflicts when using custom Readers. -* Renamed `BaseTilerFactory` to `BaseFactory` **breaking change** +* Rename `BaseTilerFactory` to `BaseFactory` **breaking change** + +* Remove useless attribute in `BaseFactory` (and moved them to `TilerFactory`) **breaking change** -* Removed useless attribute in `BaseFactory` (and moved them to `TilerFactory`) **breaking change** +* Add `crs` option to `/bounds` endpoints to enable geographic_crs selection by the user + +* `/bounds` endpoints now return a `crs: str` attribute in the response ### titiler.mosaic -* Renamed `reader` attribute to `backend` in `MosaicTilerFactory` **breaking change** +* Rename `reader` attribute to `backend` in `MosaicTilerFactory` **breaking change** + +* Add `crs` option to `/bounds` endpoints to enable geographic_crs selection by the user + +* `/bounds` endpoints now return a `crs: str` attribute in the response + +* Update `cogeo-mosaic` dependency to `>=8.0,<9.0` ### titiler.extensions @@ -77,6 +92,8 @@ * Add links for render parameters and `/map` link to **viewer** dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987) +* Update viewers to use `/info.geojson` endpoint instead of `/info` + ## 0.18.10 (2024-10-17) ### titiler.application diff --git a/docs/src/endpoints/cog.md b/docs/src/endpoints/cog.md index 584ce6726..cdcc77cf7 100644 --- a/docs/src/endpoints/cog.md +++ b/docs/src/endpoints/cog.md @@ -280,6 +280,7 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: @@ -289,17 +290,23 @@ Example: `:endpoint:/cog/info` general raster info +- QueryParams: + - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + +Example: + +- `https://myendpoint/cog/info?url=https://somewhere.com/mycog.tif` + `:endpoint:/cog/info.geojson` general raster info as a GeoJSON feature - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: -- `https://myendpoint/cog/info?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/info.geojson?url=https://somewhere.com/mycog.tif` - ### Statistics Advanced raster statistics diff --git a/docs/src/endpoints/stac.md b/docs/src/endpoints/stac.md index b3c9a949e..0c8e872b1 100644 --- a/docs/src/endpoints/stac.md +++ b/docs/src/endpoints/stac.md @@ -307,6 +307,7 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: @@ -330,11 +331,7 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** - **assets** (array[str]): asset names. Default to all available assets. - -Example: - -- `https://myendpoint/stac/info.geojson?url=https://somewhere.com/item.json&assets=B01` - + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. `:endpoint:/stac/assets` - Return the list of available assets diff --git a/docs/src/examples/notebooks/Working_with_Statistics.ipynb b/docs/src/examples/notebooks/Working_with_Statistics.ipynb index d9f2ac5e4..947d91eec 100644 --- a/docs/src/examples/notebooks/Working_with_Statistics.ipynb +++ b/docs/src/examples/notebooks/Working_with_Statistics.ipynb @@ -2,31 +2,38 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# Working with Statistics" - ], "metadata": { "collapsed": false - } + }, + "source": [ + "# Working with Statistics" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Intro\n", "\n", "Titiler allows you to get statistics and summaries of your data without having to load the entire dataset yourself. These statistics can be summaries of entire COG files, STAC items, or individual parts of the file, specified using GeoJSON.\n", "\n", - "Below, we will go over some of the statistical endpoints in Titiler - `/bounds`, `/info`, and `/statistics`.\n", + "Below, we will go over some of the statistical endpoints in Titiler - `/info` and `/statistics`.\n", "\n", "(Note: these examples will be using the `/cog` endpoint, but everything is also available for `/stac` and `/mosaicjson` unless otherwise noted)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:40.161502Z", + "start_time": "2023-04-06T14:25:40.153667Z" + }, + "collapsed": false + }, "outputs": [], "source": [ "# setup\n", @@ -35,86 +42,37 @@ "\n", "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", "cog_url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:40.153667Z", - "end_time": "2023-04-06T14:25:40.161502Z" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "## Bounds\n", - "\n", - "The `/bounds` endpoint returns the bounding box of the image/asset. These bounds are returned in the projection EPSG:4326 (WGS84), in the format `(minx, miny, maxx, maxy)`." - ], "metadata": { "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 10, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'bounds': [57.664053823239804, -20.55473177712791, 57.84021477996238, -20.25261582755764]}\n" - ] - } - ], + }, "source": [ - "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/bounds\",\n", - " params = {\n", - " \"url\": cog_url,\n", - " }\n", - ").json()\n", + "## Info\n", "\n", - "bounds = r[\"bounds\"]\n", - "print(r)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:40.781598Z", - "end_time": "2023-04-06T14:25:40.921234Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "For a bit more information, you can get summary statistics from the `/info` endpoint. This includes info such as:\n", - "- Bounds (identical to the `/bounds` endpoint)\n", - "- Min and max zoom\n", + "The `/info` endpoint returns general metadata about the image/asset.\n", + "\n", + "- Bounds\n", + "- CRS \n", "- Band metadata, such as names of the bands and their descriptions\n", "- Number of bands in the image\n", "- Overview levels\n", - "- Image width and height\n", - "\n", - "These are statistics available in the metadata of the image, so should be fast to read.\n" - ], - "metadata": { - "collapsed": false - } + "- Image width and height" + ] }, { "cell_type": "code", - "execution_count": 11, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"bounds\": [57.664053823239804, -20.55473177712791, 57.84021477996238, -20.25261582755764], \"minzoom\": 10, \"maxzoom\": 18, \"band_metadata\": [[\"b1\", {}], [\"b2\", {}], [\"b3\", {}]], \"band_descriptions\": [[\"b1\", \"\"], [\"b2\", \"\"], [\"b3\", \"\"]], \"dtype\": \"uint8\", \"nodata_type\": \"Mask\", \"colorinterp\": [\"red\", \"green\", \"blue\"], \"count\": 3, \"width\": 38628, \"driver\": \"GTiff\", \"overviews\": [2, 4, 8, 16, 32, 64, 128], \"height\": 66247}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.410135Z", + "start_time": "2023-04-06T14:25:42.355858Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", @@ -124,17 +82,13 @@ ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:42.355858Z", - "end_time": "2023-04-06T14:25:42.410135Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Statistics\n", "\n", @@ -144,23 +98,19 @@ "- Percentiles\n", "\n", "Statistics are generated both for the image as a whole and for each band individually." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 12, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"b1\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 36.94901407469342, \"count\": 574080.0, \"sum\": 21211690.0, \"std\": 48.282133573955264, \"median\": 3.0, \"majority\": 1.0, \"minority\": 246.0, \"unique\": 256.0, \"histogram\": [[330584.0, 54820.0, 67683.0, 57434.0, 30305.0, 14648.0, 9606.0, 5653.0, 2296.0, 1051.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\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 0.0, \"percentile_98\": 171.0}, \"b2\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 57.1494356187291, \"count\": 574080.0, \"sum\": 32808348.0, \"std\": 56.300819175100656, \"median\": 37.0, \"majority\": 5.0, \"minority\": 0.0, \"unique\": 256.0, \"histogram\": [[271018.0, 34938.0, 54030.0, 69429.0, 70260.0, 32107.0, 29375.0, 9697.0, 2001.0, 1225.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\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 5.0, \"percentile_98\": 180.0}, \"b3\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 51.251764562430324, \"count\": 574080.0, \"sum\": 29422613.0, \"std\": 39.65505035854822, \"median\": 36.0, \"majority\": 16.0, \"minority\": 252.0, \"unique\": 254.0, \"histogram\": [[203263.0, 150865.0, 104882.0, 42645.0, 30652.0, 25382.0, 12434.0, 2397.0, 1097.0, 463.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\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 14.0, \"percentile_98\": 158.0}}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.866905Z", + "start_time": "2023-04-06T14:25:42.816337Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", @@ -170,38 +120,30 @@ ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:42.816337Z", - "end_time": "2023-04-06T14:25:42.866905Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ - "This endpoint is far more configurable than `/bounds` and `info`. You can specify which bands to analyse, how to generate the histogram, and pre-process the image.\n", + "This endpoint is far more configurable than `/info`. You can specify which bands to analyse, how to generate the histogram, and pre-process the image.\n", "\n", "For example, if you wanted to get the statistics of the [VARI](https://www.space4water.org/space/visible-atmospherically-resistant-index-vari) of the image you can use the `expression` parameter to conduct simple band math:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 13, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"(b2-b1)/(b1+b2-b3)\": {\"min\": -1.7976931348623157e+308, \"max\": 1.7976931348623157e+308, \"mean\": null, \"count\": 574080.0, \"sum\": null, \"std\": null, \"median\": -0.15384615384615385, \"majority\": -0.4, \"minority\": -149.0, \"unique\": 18718.0, \"histogram\": [[5646.0, 10176.0, 130905.0, 97746.0, 50184.0, 95842.0, 60322.0, 21478.0, 13552.0, 12204.0], [-1.0, -0.8, -0.6, -0.3999999999999999, -0.19999999999999996, 0.0, 0.20000000000000018, 0.40000000000000013, 0.6000000000000001, 0.8, 1.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": -3.5, \"percentile_98\": 3.3870967741935485}}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:43.393610Z", + "start_time": "2023-04-06T14:25:43.304442Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", @@ -213,29 +155,29 @@ ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:43.304442Z", - "end_time": "2023-04-06T14:25:43.393610Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "Alternatively, if you would like to get statistics for only a certain area, you can specify an area via a feature or a feature collection.\n", "\n", "(Note: this endpoint is not available in the mosaicjson endpoint, only `/cog` and `/stac`)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:43.877434Z", + "start_time": "2023-04-06T14:25:43.867923Z" + }, + "collapsed": false + }, "outputs": [], "source": [ "mahebourg = \"\"\"\n", @@ -284,27 +226,19 @@ " ]\n", "}\n", "\"\"\"" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:43.867923Z", - "end_time": "2023-04-06T14:25:43.877434Z" - } - } + ] }, { "cell_type": "code", - "execution_count": 15, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Polygon\", \"coordinates\": [[[57.70358910197049, -20.384114558699935], [57.68564920588395, -20.384114558699935], [57.68209507552771, -20.39855066753664], [57.68666467170024, -20.421074640746554], [57.70341985766697, -20.434397129770545], [57.72999121319131, -20.42392955694521], [57.70358910197049, -20.384114558699935]]]}, \"properties\": {\"statistics\": {\"b1\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 88.5634794986129, \"count\": 619641.0, \"sum\": 54877563.0, \"std\": 55.18714964714274, \"median\": 77.0, \"majority\": 52.0, \"minority\": 253.0, \"unique\": 256.0, \"histogram\": [[67233.0, 110049.0, 129122.0, 90849.0, 77108.0, 44091.0, 44606.0, 37790.0, 18033.0, 760.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\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 4.0, \"percentile_98\": 208.0}, \"b2\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 112.07155594933195, \"count\": 619641.0, \"sum\": 69444131.0, \"std\": 42.64508357271268, \"median\": 107.0, \"majority\": 103.0, \"minority\": 1.0, \"unique\": 256.0, \"histogram\": [[6004.0, 31108.0, 107187.0, 126848.0, 130731.0, 73650.0, 107827.0, 33264.0, 2403.0, 619.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\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 34.0, \"percentile_98\": 189.0}, \"b3\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 84.54690377170006, \"count\": 619641.0, \"sum\": 52388728.0, \"std\": 44.64862735915829, \"median\": 77.0, \"majority\": 53.0, \"minority\": 254.0, \"unique\": 256.0, \"histogram\": [[40704.0, 130299.0, 138014.0, 85866.0, 86381.0, 91182.0, 41872.0, 4116.0, 993.0, 214.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\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 11.0, \"percentile_98\": 170.0}}}}]}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:45.709013Z", + "start_time": "2023-04-06T14:25:44.592051Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "# NOTE: This is a POST request, unlike all other requests in this example\n", "r = httpx.post(\n", @@ -312,27 +246,20 @@ " data=mahebourg,\n", " params = {\n", " \"url\": cog_url,\n", - " }\n", + " \"max_size\": 1024,\n", + " },\n", + " timeout=20,\n", ").json()\n", "\n", "print(json.dumps(r))\n" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:44.592051Z", - "end_time": "2023-04-06T14:25:45.709013Z" - } - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": {}, "outputs": [], - "source": [], - "metadata": { - "collapsed": false - } + "source": [] } ], "metadata": { @@ -344,14 +271,14 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.9.19" } }, "nbformat": 4, diff --git a/src/titiler/application/tests/routes/test_mosaic.py b/src/titiler/application/tests/routes/test_mosaic.py index 27e078289..d9acea550 100644 --- a/src/titiler/application/tests/routes/test_mosaic.py +++ b/src/titiler/application/tests/routes/test_mosaic.py @@ -57,20 +57,22 @@ def test_info(app): response = app.get("/mosaicjson/info", params={"url": MOSAICJSON_FILE}) assert response.status_code == 200 body = response.json() - assert body["minzoom"] == 7 - assert body["maxzoom"] == 9 assert body["name"] == "mosaic" # mosaic.name is not set assert body["quadkeys"] == [] + assert body["mosaic_minzoom"] == 7 + assert body["mosaic_maxzoom"] == 9 + assert body["mosaic_tilematrixset"] response = app.get("/mosaicjson/info.geojson", params={"url": MOSAICJSON_FILE}) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" body = response.json() assert body["geometry"] - assert body["properties"]["minzoom"] == 7 - assert body["properties"]["maxzoom"] == 9 assert body["properties"]["name"] == "mosaic" # mosaic.name is not set assert body["properties"]["quadkeys"] == [] + assert body["properties"]["mosaic_minzoom"] == 7 + assert body["properties"]["mosaic_maxzoom"] == 9 + assert body["properties"]["mosaic_tilematrixset"] def test_tilejson(app): diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index 34afdfe17..028b6bf63 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -32,13 +32,13 @@ classifiers = [ dynamic = ["version"] dependencies = [ "fastapi>=0.108.0", - "geojson-pydantic>=1.0,<2.0", + "geojson-pydantic>=1.1.2,<2.0", "jinja2>=2.11.2,<4.0.0", "numpy", "pydantic~=2.0", "rasterio", - "rio-tiler>=6.3.0,<7.0", - "morecantile>=5.0,<6.0", + "rio-tiler>=7.0,<8.0", + "morecantile", "simplejson", "typing_extensions>=4.6.1", ] diff --git a/src/titiler/core/tests/fixtures/cog_dateline.tif b/src/titiler/core/tests/fixtures/cog_dateline.tif new file mode 100644 index 000000000..279183018 Binary files /dev/null and b/src/titiler/core/tests/fixtures/cog_dateline.tif differ diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 7545f9a92..1ad890ef0 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -296,7 +296,8 @@ def test_TilerFactory(): response = client.get(f"/bounds?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" - assert response.json()["bounds"] + assert len(response.json()["bounds"]) == 4 + assert response.json()["crs"] response = client.get(f"/info?url={DATA_DIR}/cog.tif") assert response.status_code == 200 @@ -307,6 +308,15 @@ def test_TilerFactory(): assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" assert response.json()["type"] == "Feature" + assert "bbox" in response.json() + assert response.json()["geometry"]["type"] == "Polygon" + + response = client.get(f"/info.geojson?url={DATA_DIR}/cog_dateline.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + assert response.json()["type"] == "Feature" + assert "bbox" in response.json() + assert response.json()["geometry"]["type"] == "MultiPolygon" response = client.get( f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" @@ -744,6 +754,7 @@ def test_MultiBaseTilerFactory(rio): response = client.get(f"/bounds?url={DATA_DIR}/item.json") assert response.status_code == 200 assert len(response.json()["bounds"]) == 4 + assert response.json()["crs"] response = client.get(f"/info?url={DATA_DIR}/item.json") assert response.status_code == 200 diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index 9de930475..2a66cf3e8 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -602,6 +602,21 @@ def DstCRSParams( return None +def CRSParams( + crs: Annotated[ + Optional[str], + Query( + description="Coordinate Reference System`.", + ), + ] = None, +) -> Optional[CRS]: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) + + return None + + def BufferParams( buffer: Annotated[ Optional[float], diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 1cb9dfe76..823503be6 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -23,7 +23,7 @@ from fastapi.dependencies.utils import get_parameterless_sub_dependant from fastapi.params import Depends as DependsFunc from geojson_pydantic.features import Feature, FeatureCollection -from geojson_pydantic.geometries import Polygon +from geojson_pydantic.geometries import MultiPolygon, Polygon from morecantile import TileMatrixSet from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets @@ -34,6 +34,7 @@ from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader from rio_tiler.models import Bounds, ImageData, Info from rio_tiler.types import ColorMapType +from rio_tiler.utils import CRS_to_uri from starlette.requests import Request from starlette.responses import HTMLResponse, Response from starlette.routing import Match, compile_path, replace_params @@ -54,6 +55,7 @@ ColorFormulaParams, ColorMapParams, CoordCRSParams, + CRSParams, DatasetParams, DatasetPathParams, DefaultDependency, @@ -334,12 +336,17 @@ def bounds(self): def bounds( src_path=Depends(self.path_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return the bounds of the COG.""" with rasterio.Env(**env): with self.reader(src_path, **reader_params.as_dict()) as src_dst: - return {"bounds": src_dst.geographic_bounds} + crs = crs or WGS84_CRS + return { + "bounds": src_dst.get_geographic_bounds(crs or WGS84_CRS), + "crs": CRS_to_uri(crs) or crs.to_wkt(), + } ############################################################################ # /info @@ -379,14 +386,27 @@ def info( def info_geojson( src_path=Depends(self.path_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + geometry = MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + else: + geometry = Polygon.from_bounds(*bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), + bbox=bounds, + geometry=geometry, properties=src_dst.info(), ) @@ -686,7 +706,9 @@ def tilejson( src_path, tms=tms, **reader_params.as_dict() ) as src_dst: return { - "bounds": src_dst.geographic_bounds, + "bounds": src_dst.get_geographic_bounds( + tms.rasterio_geographic_crs + ), "minzoom": minzoom if minzoom is not None else src_dst.minzoom, "maxzoom": maxzoom if maxzoom is not None else src_dst.maxzoom, "tiles": [tiles_url], @@ -839,7 +861,7 @@ def wmts( with self.reader( src_path, tms=tms, **reader_params.as_dict() ) as src_dst: - bounds = src_dst.geographic_bounds + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) minzoom = minzoom if minzoom is not None else src_dst.minzoom maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom @@ -863,6 +885,12 @@ def wmts( else: supported_crs = tms.crs.srs + bbox_crs_type = "WGS84BoundingBox" + bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84" + if tms.rasterio_geographic_crs != WGS84_CRS: + bbox_crs_type = "BoundingBox" + bbox_crs_uri = CRS_to_uri(tms.rasterio_geographic_crs) + return self.templates.TemplateResponse( request, name="wmts.xml", @@ -872,6 +900,8 @@ def wmts( "tileMatrix": tileMatrix, "tms": tms, "supported_crs": supported_crs, + "bbox_crs_type": bbox_crs_type, + "bbox_crs_uri": bbox_crs_uri, "title": src_path if isinstance(src_path, str) else "TiTiler", "layer_name": "Dataset", "media_type": tile_format.mediatype, @@ -1173,14 +1203,27 @@ def info_geojson( src_path=Depends(self.path_dependency), asset_params=Depends(self.assets_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + geometry = MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + else: + geometry = Polygon.from_bounds(*bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), + bbox=bounds, + geometry=geometry, properties={ asset: asset_info for asset, asset_info in src_dst.info( @@ -1418,14 +1461,27 @@ def info_geojson( src_path=Depends(self.path_dependency), bands_params=Depends(self.bands_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + geometry = MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + else: + geometry = Polygon.from_bounds(*bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), + bbox=bounds, + geometry=geometry, properties=src_dst.info(**bands_params.as_dict()), ) diff --git a/src/titiler/core/titiler/core/models/responses.py b/src/titiler/core/titiler/core/models/responses.py index f79db332a..280a11894 100644 --- a/src/titiler/core/titiler/core/models/responses.py +++ b/src/titiler/core/titiler/core/models/responses.py @@ -3,7 +3,7 @@ from typing import Dict, List, Union from geojson_pydantic.features import Feature, FeatureCollection -from geojson_pydantic.geometries import Geometry, Polygon +from geojson_pydantic.geometries import Geometry, MultiPolygon, Polygon from pydantic import BaseModel from rio_tiler.models import BandStatistics, Info @@ -23,7 +23,7 @@ class Point(BaseModel): band_names: List[str] -InfoGeoJSON = Feature[Polygon, Info] +InfoGeoJSON = Feature[Union[Polygon, MultiPolygon], Info] Statistics = Dict[str, BandStatistics] @@ -42,7 +42,7 @@ class StatisticsInGeoJSON(BaseModel): # MultiBase Models MultiBaseInfo = Dict[str, Info] -MultiBaseInfoGeoJSON = Feature[Polygon, MultiBaseInfo] +MultiBaseInfoGeoJSON = Feature[Union[Polygon, MultiPolygon], MultiBaseInfo] MultiBaseStatistics = Dict[str, Statistics] MultiBaseStatisticsGeoJSON = StatisticsGeoJSON diff --git a/src/titiler/core/titiler/core/templates/wmts.xml b/src/titiler/core/titiler/core/templates/wmts.xml index ae36a247f..b86349ad5 100644 --- a/src/titiler/core/titiler/core/templates/wmts.xml +++ b/src/titiler/core/titiler/core/templates/wmts.xml @@ -37,10 +37,10 @@ {{ title }} {{ layer_name }} {{ title }} - + {{ bounds[0] }} {{ bounds[1] }} {{ bounds[2] }} {{ bounds[3] }} - + diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index d6c78f9e1..e782d5326 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -360,15 +360,15 @@ function updateShareButtonVisibility() { console.log('updateShareButtonVisibility called'); console.log('viewer_enabled:', viewer_enabled); - + const shareButton = document.getElementById('btn-share-link'); if (!shareButton) { console.log('Share button not found in the DOM'); return; } - + console.log('shareButton:', shareButton); - + if (viewer_enabled === 'true') { console.log('Setting button to visible'); shareButton.classList.remove('hidden'); @@ -823,48 +823,6 @@ } } -const addAOI = (bounds) => { - if (map.getLayer('aoi-polygon')) map.removeLayer('aoi-polygon') - if (map.getSource('aoi')) map.removeSource('aoi') - - // handle files that span accross dateline - if (bounds[0] > bounds[2]) { - map.addSource('aoi', { - 'type': 'geojson', - 'data': { - "type": "FeatureCollection", - "features": [ - bboxPolygon([-180, bounds[1], bounds[2], bounds[3]]), - bboxPolygon([bounds[0], bounds[1], 180, bounds[3]]), - ] - } - }) - - } else { - map.addSource('aoi', { - 'type': 'geojson', - 'data': { - "type": "FeatureCollection", - "features": [bboxPolygon(bounds)] - } - }) - } - - map.addLayer({ - id: 'aoi-polygon', - type: 'line', - source: 'aoi', - layout: { - 'line-cap': 'round', - 'line-join': 'round' - }, - paint: { - 'line-color': '#3bb2d0', - 'line-width': 1 - } - }) - return -} const parseParams = (w_loc) => { const param_list = w_loc.replace('?', '').split('&') @@ -884,8 +842,8 @@ }) .then(data => { console.log(data) - scope.data_type = data.dtype - scope.colormap = data.colormap + scope.data_type = data.properties.dtype + scope.colormap = data.properties.colormap if (['uint8','int8'].indexOf(scope.data_type) === -1 && !scope.colormap) document.getElementById('minmax-data').classList.remove('none') @@ -893,8 +851,8 @@ document.getElementById('data-min').value = mm[0] document.getElementById('data-max').value = mm[1] - scope.band_descriptions = data.band_descriptions - const band_descr = data.band_descriptions + scope.band_descriptions = data.properties.band_descriptions + const band_descr = data.properties.band_descriptions const nbands = band_descr.length //Populate Band (1b) selector @@ -925,15 +883,20 @@ document.getElementById('hide-arrow').classList.toggle('off') document.getElementById('menu').classList.toggle('off') - let bounds = [...data.bounds] + let bounds = [...data.bbox] // Bounds crossing dateline if (bounds[0] > bounds[2]) { bounds[0] = bounds[0] - 360 } - map.fitBounds( - [[bounds[0], bounds[1]], [bounds[2], bounds[3]]] - ) - addAOI(data.bounds) + map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]]) + map.addSource('aoi', {'type': 'geojson', 'data': data}) + map.addLayer({ + id: 'aoi-polygon', + type: 'line', + source: 'aoi', + layout: {'line-cap': 'round', 'line-join': 'round'}, + paint: {'line-color': '#3bb2d0', 'line-width': 1} + }) if (nbands === 1) { document.getElementById('3b').classList.add('disabled') @@ -966,7 +929,7 @@ document.getElementById('btn-share-link').addEventListener('click', () => { const rasterType = document.getElementById("toolbar").querySelector(".active").id; - + let params = new URLSearchParams(); params.append('url', scope.url); @@ -990,7 +953,7 @@ const path_name = `${window.location.pathname}`.replace("viewer", "WebMercatorQuad/map"); const shareUrl = `${window.location.origin}${path_name}?${params.toString()}`; - + // Create a temporary input element to copy the URL const tempInput = document.createElement('input'); tempInput.value = shareUrl; @@ -1020,7 +983,7 @@ parseFloat(document.getElementById("g-selector").selectedOptions[0].getAttribute("bidx")), parseFloat(document.getElementById("b-selector").selectedOptions[0].getAttribute("bidx")), ] - } + } // Add rescale parameter for both 1b and 3b if applicable if (["uint8", "int8"].indexOf(scope.data_type) === -1 && !scope.colormap) { diff --git a/src/titiler/extensions/titiler/extensions/templates/stac_viewer.html b/src/titiler/extensions/titiler/extensions/templates/stac_viewer.html index 71ef9f348..e31b9b5c9 100644 --- a/src/titiler/extensions/titiler/extensions/templates/stac_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/stac_viewer.html @@ -807,49 +807,6 @@ } } -const addAOI = (bounds) => { - if (map.getLayer('aoi-polygon')) map.removeLayer('aoi-polygon') - if (map.getSource('aoi')) map.removeSource('aoi') - - // handle files that span accross dateline - if (bounds[0] > bounds[2]) { - map.addSource('aoi', { - 'type': 'geojson', - 'data': { - "type": "FeatureCollection", - "features": [ - bboxPolygon([-180, bounds[1], bounds[2], bounds[3]]), - bboxPolygon([bounds[0], bounds[1], 180, bounds[3]]), - ] - } - }) - - } else { - map.addSource('aoi', { - 'type': 'geojson', - 'data': { - "type": "FeatureCollection", - "features": [bboxPolygon(bounds)] - } - }) - } - - map.addLayer({ - id: 'aoi-polygon', - type: 'line', - source: 'aoi', - layout: { - 'line-cap': 'round', - 'line-join': 'round' - }, - paint: { - 'line-color': '#3bb2d0', - 'line-width': 1 - } - }) - return -} - const updateUI = () => { const is_checked = document.getElementById("compose-switch").checked const rList = document.getElementById('r-selector') @@ -952,16 +909,6 @@ document.getElementById('data-min').value = mm[0] document.getElementById('data-max').value = mm[1] - let bounds = [...info.bounds] - // Bounds crossing dateline - if (bounds[0] > bounds[2]) { - bounds[0] = bounds[0] - 360 - } - map.fitBounds( - [[bounds[0], bounds[1]], [bounds[2], bounds[3]]] - ) - addAOI(info.bounds) - if (nbands === 1) { document.getElementById('3b').classList.add('disabled') document.getElementById('3b').classList.remove('active') @@ -997,8 +944,23 @@ document.getElementById('menu').classList.toggle('off') document.getElementById('loader').classList.toggle('off') - scope.assets = Object.entries(data).map((e) => {return e[0]}) - scope.dataset_info = data + scope.assets = Object.entries(data.properties).map((e) => {return e[0]}) + scope.dataset_info = data.properties + + let bounds = [...data.bbox] + // Bounds crossing dateline + if (bounds[0] > bounds[2]) { + bounds[0] = bounds[0] - 360 + } + map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]]) + map.addSource('aoi', {'type': 'geojson', 'data': data}) + map.addLayer({ + id: 'aoi-polygon', + type: 'line', + source: 'aoi', + layout: {'line-cap': 'round', 'line-join': 'round'}, + paint: {'line-color': '#3bb2d0', 'line-width': 1} + }) const assetList = document.getElementById('asset-selector') for (var i = 0; i < scope.assets.length; i++) { diff --git a/src/titiler/extensions/titiler/extensions/viewer.py b/src/titiler/extensions/titiler/extensions/viewer.py index daa3e9bde..4b88124c5 100644 --- a/src/titiler/extensions/titiler/extensions/viewer.py +++ b/src/titiler/extensions/titiler/extensions/viewer.py @@ -33,7 +33,7 @@ def cog_viewer(request: Request): "tilejson_endpoint": factory.url_for( request, "tilejson", tileMatrixSetId="WebMercatorQuad" ), - "info_endpoint": factory.url_for(request, "info"), + "info_endpoint": factory.url_for(request, "info_geojson"), "statistics_endpoint": factory.url_for(request, "statistics"), "viewer_enabled": getattr(factory, "add_viewer", False), }, @@ -60,7 +60,7 @@ def stac_viewer(request: Request): "tilejson_endpoint": factory.url_for( request, "tilejson", tileMatrixSetId="WebMercatorQuad" ), - "info_endpoint": factory.url_for(request, "info"), + "info_endpoint": factory.url_for(request, "info_geojson"), "statistics_endpoint": factory.url_for(request, "asset_statistics"), }, media_type="text/html", diff --git a/src/titiler/extensions/titiler/extensions/wms.py b/src/titiler/extensions/titiler/extensions/wms.py index 916d909e6..71dd1e206 100644 --- a/src/titiler/extensions/titiler/extensions/wms.py +++ b/src/titiler/extensions/titiler/extensions/wms.py @@ -11,6 +11,7 @@ from attrs import define, field from fastapi import Depends, HTTPException from rasterio.crs import CRS +from rio_tiler.constants import WGS84_CRS from rio_tiler.models import ImageData from rio_tiler.mosaic import mosaic_reader from rio_tiler.mosaic.methods.base import MosaicMethodBase @@ -381,7 +382,7 @@ def wms( # noqa: C901 layers_dict[layer]["bounds"] = src_dst.bounds layers_dict[layer][ "bounds_wgs84" - ] = src_dst.geographic_bounds + ] = src_dst.get_geographic_bounds(WGS84_CRS) layers_dict[layer][ "abstract" ] = src_dst.info().model_dump_json() diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index fd1883edc..8df0812ed 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ "titiler.core==0.19.0.dev", - "cogeo-mosaic>=7.0,<8.0", + "cogeo-mosaic>=8.0,<9.0", ] [project.optional-dependencies] diff --git a/src/titiler/mosaic/tests/test_factory.py b/src/titiler/mosaic/tests/test_factory.py index 5dfa49762..ed53cc97f 100644 --- a/src/titiler/mosaic/tests/test_factory.py +++ b/src/titiler/mosaic/tests/test_factory.py @@ -75,7 +75,8 @@ def test_MosaicTilerFactory(): params={"url": mosaic_file}, ) assert response.status_code == 200 - assert response.json()["bounds"] + assert len(response.json()["bounds"]) == 4 + assert response.json()["crs"] response = client.get( "/mosaic/info", diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index 366e10546..12b7f1d5e 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -11,7 +11,7 @@ from cogeo_mosaic.mosaic import MosaicJSON from fastapi import Depends, HTTPException, Path, Query from geojson_pydantic.features import Feature -from geojson_pydantic.geometries import Polygon +from geojson_pydantic.geometries import MultiPolygon, Polygon from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets from pydantic import Field @@ -21,6 +21,7 @@ from rio_tiler.mosaic.methods import PixelSelectionMethod from rio_tiler.mosaic.methods.base import MosaicMethodBase from rio_tiler.types import ColorMapType +from rio_tiler.utils import CRS_to_uri from starlette.requests import Request from starlette.responses import HTMLResponse, Response from starlette.templating import Jinja2Templates @@ -33,6 +34,7 @@ ColorFormulaParams, ColorMapParams, CoordCRSParams, + CRSParams, DatasetParams, DatasetPathParams, DefaultDependency, @@ -172,6 +174,7 @@ def bounds( src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return the bounds of the MosaicJSON.""" @@ -182,7 +185,11 @@ def bounds( reader_options=reader_params.as_dict(), **backend_params.as_dict(), ) as src_dst: - return {"bounds": src_dst.geographic_bounds} + crs = crs or WGS84_CRS + return { + "bounds": src_dst.get_geographic_bounds(crs or WGS84_CRS), + "crs": CRS_to_uri(crs) or crs.to_wkt(), + } ############################################################################ # /info @@ -227,6 +234,7 @@ def info_geojson( src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return mosaic's basic info as a GeoJSON feature.""" @@ -237,11 +245,22 @@ def info_geojson( reader_options=reader_params.as_dict(), **backend_params.as_dict(), ) as src_dst: - info = src_dst.info() + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + geometry = MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + else: + geometry = Polygon.from_bounds(*bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*info.bounds), - properties=info, + bbox=bounds, + geometry=geometry, + properties=src_dst.info(), ) ############################################################################ @@ -571,6 +590,12 @@ def wmts( Optional[int], Query(description="Overwrite default maxzoom."), ] = None, + use_epsg: Annotated[ + bool, + Query( + description="Use EPSG code, not opengis.net, for the ows:SupportedCRS in the TileMatrixSet (set to True to enable ArcMap compatability)" + ), + ] = False, layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), pixel_selection=Depends(self.pixel_selection_dependency), @@ -602,6 +627,7 @@ def wmts( "minzoom", "maxzoom", "service", + "use_epsg", "request", ] qs = [ @@ -621,7 +647,7 @@ def wmts( reader_options=reader_params.as_dict(), **backend_params.as_dict(), ) as src_dst: - bounds = src_dst.geographic_bounds + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) minzoom = minzoom if minzoom is not None else src_dst.minzoom maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom @@ -640,6 +666,17 @@ def wmts( """ tileMatrix.append(tm) + if use_epsg: + supported_crs = f"EPSG:{tms.crs.to_epsg()}" + else: + supported_crs = tms.crs.srs + + bbox_crs_type = "WGS84BoundingBox" + bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84" + if tms.rasterio_geographic_crs != WGS84_CRS: + bbox_crs_type = "BoundingBox" + bbox_crs_uri = CRS_to_uri(tms.rasterio_geographic_crs) + return self.templates.TemplateResponse( request, name="wmts.xml", @@ -648,6 +685,9 @@ def wmts( "bounds": bounds, "tileMatrix": tileMatrix, "tms": tms, + "supported_crs": supported_crs, + "bbox_crs_type": bbox_crs_type, + "bbox_crs_uri": bbox_crs_uri, "title": src_path if isinstance(src_path, str) else "TiTiler Mosaic",