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",