Skip to content

Commit 492fa84

Browse files
wakame1367claudevincentsarago
authored
fix: fall back to WKT for WMTS BoundingBox crs when CRS cannot be resolved to a URN (#1448)
* fix: fall back to WKT for WMTS BoundingBox crs when CRS cannot be resolved to a URN Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: satisfy mypy by narrowing crs urn type before assignment Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: vincentsarago <vincent.sarago@gmail.com>
1 parent dccc3c1 commit 492fa84

5 files changed

Lines changed: 93 additions & 2 deletions

File tree

‎CHANGES.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## Unreleased
44

5+
### titiler.extensions
6+
7+
* fall back to WKT (with a warning) for the WMTS `BoundingBox` crs attribute when the CRS cannot be resolved to a URN (https://github.com/developmentseed/titiler/issues/1043)
8+
9+
### titiler.mosaic
10+
11+
* fall back to WKT (with a warning) for the WMTS `BoundingBox` crs attribute when the CRS cannot be resolved to a URN (https://github.com/developmentseed/titiler/issues/1043)
12+
513
## 2.0.5 (2026-06-18)
614

715
## What's Changed

‎src/titiler/extensions/tests/test_wmts.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import io
44
import os
5+
import xml.etree.ElementTree as ET
56

7+
import pytest
68
import rasterio
79
from fastapi import FastAPI
810
from owslib.wmts import WebMapTileService
11+
from rasterio.crs import CRS
12+
from rio_tiler.utils import CRS_to_urn
913
from starlette.testclient import TestClient
1014

1115
from titiler.core.factory import TilerFactory
@@ -115,6 +119,33 @@ def test_wmtsExtension():
115119
)
116120

117121

122+
def test_wmtsExtension_crs_without_urn():
123+
"""Test BoundingBox CRS fallback to WKT when the CRS cannot be resolved to a URN.
124+
125+
ref: https://github.com/developmentseed/titiler/issues/1043
126+
"""
127+
# Custom CRS (tweaked polar stereographic) which cannot be resolved to an authority
128+
custom_crs = CRS.from_proj4(
129+
"+proj=stere +lat_0=90 +lat_ts=71.3 +lon_0=-42.5 +x_0=0 +y_0=0 +a=6378137.1 +b=6356752.3 +units=m +no_defs"
130+
)
131+
assert CRS_to_urn(custom_crs) is None
132+
133+
tiler_plus_wmts = TilerFactory(extensions=[wmtsExtension(crs=custom_crs)])
134+
135+
app = FastAPI()
136+
app.include_router(tiler_plus_wmts.router)
137+
with TestClient(app) as client:
138+
with pytest.warns(UserWarning, match="Could not resolve a URN for CRS"):
139+
response = client.get(f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif")
140+
assert response.status_code == 200
141+
142+
root = ET.fromstring(response.content)
143+
bboxes = root.findall(".//{http://www.opengis.net/ows/1.1}BoundingBox")
144+
assert bboxes
145+
for bbox in bboxes:
146+
assert bbox.attrib["crs"] == custom_crs.to_wkt()
147+
148+
118149
def test_wmtsExtension_with_renders():
119150
"""Test wmtsExtension class with Renders."""
120151
tiler_plus_wmts = TilerFactory(

‎src/titiler/extensions/titiler/extensions/wmts.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,15 @@ def wmts( # noqa: C901
189189
wmts_bbox = bounds
190190
if geographic_crs != WGS84_CRS:
191191
bbox_crs_type = "BoundingBox"
192-
bbox_crs_uri = CRS_to_urn(geographic_crs) # type: ignore
192+
crs_urn = CRS_to_urn(geographic_crs)
193+
if not crs_urn:
194+
warnings.warn(
195+
f"Could not resolve a URN for CRS '{geographic_crs}', falling back to WKT for the BoundingBox crs attribute",
196+
UserWarning,
197+
stacklevel=2,
198+
)
199+
crs_urn = geographic_crs.to_wkt()
200+
bbox_crs_uri = crs_urn
193201
# WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order
194202
proj_crs = rio_crs_to_pyproj(geographic_crs)
195203
if crs_axis_inverted(proj_crs):

‎src/titiler/mosaic/tests/test_factory.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import tempfile
5+
import xml.etree.ElementTree as ET
56
from contextlib import contextmanager
67
from dataclasses import dataclass
78
from io import BytesIO
@@ -11,12 +12,15 @@
1112
import attr
1213
import morecantile
1314
import numpy
15+
import pytest
1416
from cogeo_mosaic.backends import FileBackend
1517
from cogeo_mosaic.backends import MosaicBackend as MosaicJSONBackend
1618
from cogeo_mosaic.mosaic import MosaicJSON
1719
from fastapi import FastAPI, Query
1820
from owslib.wmts import WebMapTileService
21+
from rasterio.crs import CRS
1922
from rio_tiler.mosaic.methods import PixelSelectionMethod
23+
from rio_tiler.utils import CRS_to_urn
2024
from starlette.testclient import TestClient
2125

2226
from titiler.core.dependencies import DefaultDependency
@@ -884,6 +888,38 @@ def test_wmts_extension_mosaic():
884888
)
885889

886890

891+
def test_wmts_extension_mosaic_crs_without_urn():
892+
"""Test BoundingBox CRS fallback to WKT when the CRS cannot be resolved to a URN.
893+
894+
ref: https://github.com/developmentseed/titiler/issues/1043
895+
"""
896+
# Custom CRS (tweaked polar stereographic) which cannot be resolved to an authority
897+
custom_crs = CRS.from_proj4(
898+
"+proj=stere +lat_0=90 +lat_ts=71.3 +lon_0=-42.5 +x_0=0 +y_0=0 +a=6378137.1 +b=6356752.3 +units=m +no_defs"
899+
)
900+
assert CRS_to_urn(custom_crs) is None
901+
902+
mosaic = MosaicTilerFactory(
903+
backend=MosaicJSONBackend,
904+
extensions=[wmtsExtension(crs=custom_crs)],
905+
)
906+
app = FastAPI()
907+
app.include_router(mosaic.router)
908+
add_exception_handlers(app, MOSAIC_STATUS_CODES)
909+
910+
with TestClient(app) as client:
911+
with tmpmosaic() as mosaic_file:
912+
with pytest.warns(UserWarning, match="Could not resolve a URN for CRS"):
913+
response = client.get(f"/WMTSCapabilities.xml?url={mosaic_file}")
914+
assert response.status_code == 200
915+
916+
root = ET.fromstring(response.content)
917+
bboxes = root.findall(".//{http://www.opengis.net/ows/1.1}BoundingBox")
918+
assert bboxes
919+
for bbox in bboxes:
920+
assert bbox.attrib["crs"] == custom_crs.to_wkt()
921+
922+
887923
def test_mosaic_statistics_featurecollection():
888924
"""Test statistics endpoint with FeatureCollection containing multiple distinct features.
889925

‎src/titiler/mosaic/titiler/mosaic/extensions/wmts.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,15 @@ def wmts( # noqa: C901
200200
wmts_bbox = bounds
201201
if geographic_crs != WGS84_CRS:
202202
bbox_crs_type = "BoundingBox"
203-
bbox_crs_uri = CRS_to_urn(geographic_crs) # type: ignore
203+
crs_urn = CRS_to_urn(geographic_crs)
204+
if not crs_urn:
205+
warnings.warn(
206+
f"Could not resolve a URN for CRS '{geographic_crs}', falling back to WKT for the BoundingBox crs attribute",
207+
UserWarning,
208+
stacklevel=2,
209+
)
210+
crs_urn = geographic_crs.to_wkt()
211+
bbox_crs_uri = crs_urn
204212
# WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order
205213
proj_crs = rio_crs_to_pyproj(geographic_crs)
206214
if crs_axis_inverted(proj_crs):

0 commit comments

Comments
 (0)