From 0110e5b81479486686f6886ee88da8f37b8abe78 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Mon, 10 Aug 2020 21:22:22 -0400 Subject: [PATCH] use url_for for route matching, refactor wmts endpoints (#76) --- tests/routes/test_cog.py | 5 + tests/routes/test_mosaic.py | 4 +- titiler/endpoints/cog.py | 80 ++++++++++------ titiler/endpoints/mosaic.py | 182 ++++++++++++++++++++---------------- titiler/endpoints/stac.py | 142 +++++++++++++++++++++++----- titiler/endpoints/tms.py | 18 ++-- titiler/models/cog.py | 5 +- titiler/templates/wmts.xml | 10 +- 8 files changed, 292 insertions(+), 154 deletions(-) diff --git a/tests/routes/test_cog.py b/tests/routes/test_cog.py index d7e03ffc9..c9d213453 100644 --- a/tests/routes/test_cog.py +++ b/tests/routes/test_cog.py @@ -83,6 +83,11 @@ def test_wmts(reader, app): assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" assert response.headers["Cache-Control"] == "public, max-age=3600" + assert ( + "http://testserver/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif" + in response.content.decode() + ) + assert "cogeo" in response.content.decode() assert ( "http://testserver/cog/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@1x.png?url=https" in response.content.decode() diff --git a/tests/routes/test_mosaic.py b/tests/routes/test_mosaic.py index 2c1ab8424..1adacae28 100644 --- a/tests/routes/test_mosaic.py +++ b/tests/routes/test_mosaic.py @@ -126,8 +126,8 @@ def test_tilejson(app): TileJSON(**body) assert ( - body["tiles"][0] - == "http://testserver/mosaicjson/tiles/WebMercatorQuad/{z}/{x}/{y}@1x" + "http://testserver/mosaicjson/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=" + in body["tiles"][0] ) assert body["minzoom"] == mosaicjson["minzoom"] assert body["maxzoom"] == mosaicjson["maxzoom"] diff --git a/titiler/endpoints/cog.py b/titiler/endpoints/cog.py index a34e06e2d..d164efa50 100644 --- a/titiler/endpoints/cog.py +++ b/titiler/endpoints/cog.py @@ -173,7 +173,7 @@ async def cog_tile( content = utils.reformat( tile, mask, - img_format=format, + format, colormap=colormap, transform=dst_transform, crs=tms.crs, @@ -231,7 +231,7 @@ async def cog_preview( timings.append(("Post-process", t.elapsed)) with utils.Timer() as t: - content = utils.reformat(data, mask, img_format=format, colormap=colormap) + content = utils.reformat(data, mask, format, colormap=colormap) timings.append(("Format", t.elapsed)) if timings: @@ -287,7 +287,7 @@ async def cog_part( timings.append(("Post-process", t.elapsed)) with utils.Timer() as t: - content = utils.reformat(data, mask, img_format=format, colormap=colormap) + content = utils.reformat(data, mask, format, colormap=colormap) timings.append(("Format", t.elapsed)) if timings: @@ -365,21 +365,26 @@ async def cog_tilejson( maxzoom: Optional[int] = Query(None, description="Overwrite default maxzoom."), ): """Return TileJSON document for a COG.""" - scheme = request.url.scheme - host = request.headers["host"] + kwargs = { + "z": "{z}", + "x": "{x}", + "y": "{y}", + "scale": tile_scale, + "TileMatrixSetId": TileMatrixSetId.name, + } + if tile_format: + kwargs["format"] = tile_format.value - kwargs = dict(request.query_params) - kwargs.pop("tile_format", None) - kwargs.pop("tile_scale", None) - kwargs.pop("TileMatrixSetId", None) - kwargs.pop("minzoom", None) - kwargs.pop("maxzoom", None) + q = dict(request.query_params) + q.pop("TileMatrixSetId", None) + q.pop("tile_format", None) + q.pop("tile_scale", None) + q.pop("minzoom", None) + q.pop("maxzoom", None) + qs = urlencode(list(q.items())) - qs = urlencode(list(kwargs.items())) - if tile_format: - tile_url = f"{scheme}://{host}/cog/tiles/{TileMatrixSetId.name}/{{z}}/{{x}}/{{y}}@{tile_scale}x.{tile_format}?{qs}" - else: - tile_url = f"{scheme}://{host}/cog/tiles/{TileMatrixSetId.name}/{{z}}/{{x}}/{{y}}@{tile_scale}x?{qs}" + tiles_url = request.url_for("cog_tile", **kwargs).replace("\\", "") + tiles_url += f"?{qs}" tms = morecantile.tms.get(TileMatrixSetId.name) with COGReader(url, tms=tms) as cog: @@ -392,7 +397,7 @@ async def cog_tilejson( "minzoom": minzoom or cog.minzoom, "maxzoom": maxzoom or cog.maxzoom, "name": os.path.basename(url), - "tiles": [tile_url], + "tiles": [tiles_url], } return tjson @@ -402,7 +407,7 @@ async def cog_tilejson( @router.get( "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse, tags=["OGC"], ) -def wmts( +def cog_wmts( request: Request, TileMatrixSetId: TileMatrixSetNames = Query( TileMatrixSetNames.WebMercatorQuad, # type: ignore @@ -415,20 +420,35 @@ def wmts( tile_scale: int = Query( 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." ), + minzoom: Optional[int] = Query(None, description="Overwrite default minzoom."), + maxzoom: Optional[int] = Query(None, description="Overwrite default maxzoom."), ): """OGC WMTS endpoint.""" - scheme = request.url.scheme - host = request.headers["host"] - endpoint = f"{scheme}://{host}/cog" - - kwargs = dict(request.query_params) - kwargs.pop("tile_format", None) - kwargs.pop("tile_scale", None) - qs = urlencode(list(kwargs.items())) + kwargs = { + "z": "{TileMatrix}", + "x": "{TileCol}", + "y": "{TileRow}", + "scale": tile_scale, + "format": tile_format.value, + "TileMatrixSetId": TileMatrixSetId.name, + } + tiles_endpoint = request.url_for("cog_tile", **kwargs) + q = dict(request.query_params) + q.pop("TileMatrixSetId", None) + q.pop("tile_format", None) + q.pop("tile_scale", None) + q.pop("minzoom", None) + q.pop("maxzoom", None) + q.pop("SERVICE", None) + q.pop("REQUEST", None) + qs = urlencode(list(q.items())) + tiles_endpoint += f"?{qs}" tms = morecantile.tms.get(TileMatrixSetId.name) with COGReader(url, tms=tms) as cog: - minzoom, maxzoom, bounds = cog.minzoom, cog.maxzoom, cog.bounds + bounds = cog.bounds + minzoom = minzoom or cog.minzoom + maxzoom = maxzoom or cog.maxzoom media_type = ImageMimeTypes[tile_format.value].value @@ -447,18 +467,16 @@ def wmts( """ tileMatrix.append(tm) - tile_ext = f"@{tile_scale}x.{tile_format.value}" return templates.TemplateResponse( "wmts.xml", { "request": request, - "endpoint": endpoint, + "tiles_endpoint": tiles_endpoint, "bounds": bounds, "tileMatrix": tileMatrix, "tms": tms, "title": "Cloud Optimized GeoTIFF", - "query_string": qs, - "tile_format": tile_ext, + "layer_name": "cogeo", "media_type": media_type, }, media_type=MimeTypes.xml.value, diff --git a/titiler/endpoints/mosaic.py b/titiler/endpoints/mosaic.py index c96627aa7..dfd75bc6a 100644 --- a/titiler/endpoints/mosaic.py +++ b/titiler/endpoints/mosaic.py @@ -115,72 +115,6 @@ def mosaicjson_info(mosaic_path: str = Depends(MosaicPath)): return response -@router.get( - "/tilejson.json", - response_model=TileJSON, - responses={200: {"description": "Return a tilejson"}}, - response_model_exclude_none=True, -) -def mosaic_tilejson( - request: Request, - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - mosaic_path: str = Depends(MosaicPath), -): - """Create TileJSON""" - kwargs = {"z": "{z}", "x": "{x}", "y": "{y}", "scale": tile_scale} - if tile_format: - kwargs["format"] = tile_format - tile_url = request.url_for("mosaic_tile", **kwargs).replace("\\", "") - - with MosaicBackend(mosaic_path) as mosaic: - tjson = TileJSON(**mosaic.metadata, tiles=[tile_url]) - - return tjson - - -@router.get( - r"/point/{lon},{lat}", - responses={200: {"description": "Return a value for a point"}}, -) -async def mosaic_point( - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), - bidx: Optional[str] = Query( - None, title="Band indexes", description="comma (',') delimited band indexes", - ), - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="rio-tiler's band math expression (e.g B1/B2)", - ), - mosaic_path: str = Depends(MosaicPath), -): - """Get Point value for a MosaicJSON.""" - indexes = tuple(int(s) for s in re.findall(r"\d+", bidx)) if bidx else None - - timings = [] - headers: Dict[str, str] = {} - threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) - - with utils.Timer() as t: - with MosaicBackend(mosaic_path) as mosaic: - values = mosaic.point(lon, lat, indexes=indexes, threads=threads) - - timings.append(("Read-values", t.elapsed)) - - if timings: - headers["X-Server-Timings"] = "; ".join( - ["{} - {:0.2f}".format(name, time * 1000) for (name, time) in timings] - ) - - return {"coordinates": [lon, lat], "values": values} - - @router.get(r"/tiles/{z}/{x}/{y}", **img_endpoint_params) @router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) @router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) @@ -249,7 +183,7 @@ async def mosaic_tile( with utils.Timer() as t: content = utils.reformat( - tile, mask, img_format=format, colormap=image_params.color_map, **opts + tile, mask, format, colormap=image_params.color_map, **opts ) timings.append(("Format", t.elapsed)) @@ -266,8 +200,84 @@ async def mosaic_tile( ) +@router.get( + r"/point/{lon},{lat}", + responses={200: {"description": "Return a value for a point"}}, +) +async def mosaic_point( + lon: float = Path(..., description="Longitude"), + lat: float = Path(..., description="Latitude"), + bidx: Optional[str] = Query( + None, title="Band indexes", description="comma (',') delimited band indexes", + ), + expression: Optional[str] = Query( + None, + title="Band Math expression", + description="rio-tiler's band math expression (e.g B1/B2)", + ), + mosaic_path: str = Depends(MosaicPath), +): + """Get Point value for a MosaicJSON.""" + indexes = tuple(int(s) for s in re.findall(r"\d+", bidx)) if bidx else None + + timings = [] + headers: Dict[str, str] = {} + threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) + + with utils.Timer() as t: + with MosaicBackend(mosaic_path) as mosaic: + values = mosaic.point(lon, lat, indexes=indexes, threads=threads) + + timings.append(("Read-values", t.elapsed)) + + if timings: + headers["X-Server-Timings"] = "; ".join( + ["{} - {:0.2f}".format(name, time * 1000) for (name, time) in timings] + ) + + return {"coordinates": [lon, lat], "values": values} + + +@router.get( + "/tilejson.json", + response_model=TileJSON, + responses={200: {"description": "Return a tilejson"}}, + response_model_exclude_none=True, +) +def mosaic_tilejson( + request: Request, + tile_scale: int = Query( + 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + tile_format: Optional[ImageType] = Query( + None, description="Output image type. Default is auto." + ), + minzoom: Optional[int] = Query(None, description="Overwrite default minzoom."), + maxzoom: Optional[int] = Query(None, description="Overwrite default maxzoom."), + mosaic_path: str = Depends(MosaicPath), +): + """Create TileJSON""" + kwargs = {"z": "{z}", "x": "{x}", "y": "{y}", "scale": tile_scale} + if tile_format: + kwargs["format"] = tile_format + tiles_url = request.url_for("mosaic_tile", **kwargs).replace("\\", "") + + q = dict(request.query_params) + q.pop("tile_format", None) + q.pop("tile_scale", None) + q.pop("minzoom", None) + q.pop("maxzoom", None) + qs = urlencode(list(q.items())) + tiles_url += f"?{qs}" + + with MosaicBackend(mosaic_path) as mosaic: + tjson = TileJSON(**mosaic.metadata, tiles=[tiles_url]) + + return tjson + + @router.get("/WMTSCapabilities.xml", response_class=XMLResponse, tags=["OGC"]) -def wmts( +def mosaic_wmts( request: Request, tile_format: ImageType = Query( ImageType.png, description="Output image type. Default is png." @@ -275,20 +285,34 @@ def wmts( tile_scale: int = Query( 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." ), + minzoom: Optional[int] = Query(None, description="Overwrite default minzoom."), + maxzoom: Optional[int] = Query(None, description="Overwrite default maxzoom."), mosaic_path: str = Depends(MosaicPath), ): """OGC WMTS endpoint.""" - endpoint = request.url_for("read_mosaicjson") - - kwargs = dict(request.query_params) - kwargs.pop("tile_format", None) - kwargs.pop("tile_scale", None) - qs = urlencode(list(kwargs.items())) + kwargs = { + "z": "{TileMatrix}", + "x": "{TileCol}", + "y": "{TileRow}", + "scale": tile_scale, + "format": tile_format.value, + } + tiles_endpoint = request.url_for("mosaic_tile", **kwargs) + + q = dict(request.query_params) + q.pop("tile_format", None) + q.pop("tile_scale", None) + q.pop("minzoom", None) + q.pop("maxzoom", None) + q.pop("SERVICE", None) + q.pop("REQUEST", None) + qs = urlencode(list(q.items())) + tiles_endpoint += f"?{qs}" tms = morecantile.tms.get("WebMercatorQuad") with MosaicBackend(mosaic_path) as mosaic: - minzoom = mosaic.mosaic_def.minzoom - maxzoom = mosaic.mosaic_def.maxzoom + minzoom = minzoom or mosaic.mosaic_def.minzoom + maxzoom = maxzoom or mosaic.mosaic_def.maxzoom bounds = mosaic.mosaic_def.bounds media_type = ImageMimeTypes[tile_format.value].value @@ -308,18 +332,16 @@ def wmts( """ tileMatrix.append(tm) - tile_ext = f"@{tile_scale}x.{tile_format.value}" return templates.TemplateResponse( "wmts.xml", { "request": request, - "endpoint": endpoint, + "tiles_endpoint": tiles_endpoint, "bounds": bounds, "tileMatrix": tileMatrix, "tms": tms, "title": "Cloud Optimized GeoTIFF", - "query_string": qs, - "tile_format": tile_ext, + "layer_name": "Mosaic", "media_type": media_type, }, media_type=MimeTypes.xml.value, diff --git a/titiler/endpoints/stac.py b/titiler/endpoints/stac.py index 978d6538a..2e7464abc 100644 --- a/titiler/endpoints/stac.py +++ b/titiler/endpoints/stac.py @@ -22,14 +22,17 @@ from titiler.models.cog import cogBounds, cogInfo, cogMetadata from titiler.models.mapbox import TileJSON from titiler.ressources.common import img_endpoint_params -from titiler.ressources.enums import ImageMimeTypes, ImageType +from titiler.ressources.enums import ImageMimeTypes, ImageType, MimeTypes +from titiler.ressources.responses import XMLResponse from fastapi import APIRouter, Depends, Path, Query from starlette.requests import Request from starlette.responses import Response +from starlette.templating import Jinja2Templates router = APIRouter() +templates = Jinja2Templates(directory="titiler/templates") @router.get( @@ -170,7 +173,7 @@ async def stac_tile( content = utils.reformat( tile, mask, - img_format=format, + format, colormap=image_params.color_map, transform=dst_transform, crs=tms.crs, @@ -229,9 +232,7 @@ async def stac_preview( timings.append(("Post-process", t.elapsed)) with utils.Timer() as t: - content = utils.reformat( - data, mask, img_format=format, colormap=image_params.color_map, - ) + content = utils.reformat(data, mask, format, colormap=image_params.color_map) timings.append(("Format", t.elapsed)) if timings: @@ -285,9 +286,7 @@ async def stac_part( timings.append(("Post-process", t.elapsed)) with utils.Timer() as t: - content = utils.reformat( - data, mask, img_format=format, colormap=image_params.color_map - ) + content = utils.reformat(data, mask, format, colormap=image_params.color_map) timings.append(("Format", t.elapsed)) if timings: @@ -385,24 +384,29 @@ async def stac_tilejson( maxzoom: Optional[int] = Query(None, description="Overwrite default maxzoom."), ): """Return a TileJSON document for a STAC item.""" - scheme = request.url.scheme - host = request.headers["host"] - - kwargs = dict(request.query_params) - kwargs.pop("tile_format", None) - kwargs.pop("tile_scale", None) - kwargs.pop("TileMatrixSetId", None) - kwargs.pop("minzoom", None) - kwargs.pop("maxzoom", None) - if not expression and not assets: raise MissingAssets("Expression or Assets HAVE to be set in the queryString.") - qs = urlencode(list(kwargs.items())) + kwargs = { + "z": "{z}", + "x": "{x}", + "y": "{y}", + "scale": tile_scale, + "TileMatrixSetId": TileMatrixSetId.name, + } if tile_format: - tile_url = f"{scheme}://{host}/stac/tiles/{TileMatrixSetId.name}/{{z}}/{{x}}/{{y}}@{tile_scale}x.{tile_format}?{qs}" - else: - tile_url = f"{scheme}://{host}/stac/tiles/{TileMatrixSetId.name}/{{z}}/{{x}}/{{y}}@{tile_scale}x?{qs}" + kwargs["format"] = tile_format.value + + q = dict(request.query_params) + q.pop("tile_format", None) + q.pop("tile_scale", None) + q.pop("TileMatrixSetId", None) + q.pop("minzoom", None) + q.pop("maxzoom", None) + qs = urlencode(list(q.items())) + + tiles_url = request.url_for("stac_tile", **kwargs).replace("\\", "") + tiles_url += f"?{qs}" tms = morecantile.tms.get(TileMatrixSetId.name) with STACReader(url, tms=tms) as stac: @@ -415,7 +419,99 @@ async def stac_tilejson( "minzoom": minzoom or stac.minzoom, "maxzoom": maxzoom or stac.maxzoom, "name": os.path.basename(url), - "tiles": [tile_url], + "tiles": [tiles_url], } return tjson + + +@router.get("/WMTSCapabilities.xml", response_class=XMLResponse, tags=["OGC"]) +@router.get( + "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse, tags=["OGC"], +) +def stac_wmts( + request: Request, + TileMatrixSetId: TileMatrixSetNames = Query( + TileMatrixSetNames.WebMercatorQuad, # type: ignore + description="TileMatrixSet Name (default: 'WebMercatorQuad')", + ), + url: str = Query(..., description="STAC Item URL."), + assets: str = Query("", description="comma (,) separated list of asset names."), + expression: Optional[str] = Query( + None, + title="Band Math expression", + description="rio-tiler's band math expression (e.g B1/B2)", + ), + tile_format: ImageType = Query( + ImageType.png, description="Output image type. Default is png." + ), + tile_scale: int = Query( + 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + minzoom: Optional[int] = Query(None, description="Overwrite default minzoom."), + maxzoom: Optional[int] = Query(None, description="Overwrite default maxzoom."), +): + """OGC WMTS endpoint.""" + if not expression and not assets: + raise MissingAssets("Expression or Assets HAVE to be set in the queryString.") + + kwargs = { + "z": "{TileMatrix}", + "x": "{TileCol}", + "y": "{TileRow}", + "scale": tile_scale, + "format": tile_format.value, + "TileMatrixSetId": TileMatrixSetId.name, + } + tiles_endpoint = request.url_for("stac_tile", **kwargs) + q = dict(request.query_params) + q.pop("TileMatrixSetId", None) + q.pop("tile_format", None) + q.pop("tile_scale", None) + q.pop("minzoom", None) + q.pop("maxzoom", None) + q.pop("SERVICE", None) + q.pop("REQUEST", None) + qs = urlencode(list(q.items())) + tiles_endpoint += f"?{qs}" + + tms = morecantile.tms.get(TileMatrixSetId.name) + with STACReader(url, tms=tms) as stac: + bounds = stac.bounds + center = list(stac.center) + if minzoom: + center[-1] = minzoom + minzoom = minzoom or stac.minzoom + maxzoom = maxzoom or stac.maxzoom + + media_type = ImageMimeTypes[tile_format.value].value + + tileMatrix = [] + for zoom in range(minzoom, maxzoom + 1): + matrix = tms.matrix(zoom) + tm = f""" + + {matrix.identifier} + {matrix.scaleDenominator} + {matrix.topLeftCorner[0]} {matrix.topLeftCorner[1]} + {matrix.tileWidth} + {matrix.tileHeight} + {matrix.matrixWidth} + {matrix.matrixHeight} + """ + tileMatrix.append(tm) + + return templates.TemplateResponse( + "wmts.xml", + { + "request": request, + "tiles_endpoint": tiles_endpoint, + "bounds": bounds, + "tileMatrix": tileMatrix, + "tms": tms, + "title": "Spatial Temporal Catalog", + "layer_name": "stac", + "media_type": media_type, + }, + media_type=MimeTypes.xml.value, + ) diff --git a/titiler/endpoints/tms.py b/titiler/endpoints/tms.py index 7ad55b09e..90c29f272 100644 --- a/titiler/endpoints/tms.py +++ b/titiler/endpoints/tms.py @@ -20,30 +20,28 @@ response_model_exclude_none=True, tags=["TileMatrixSets"], ) -async def tms_list(request: Request): +async def TileMatrixSet_list(request: Request): """ Return list of supported TileMatrixSets. Specs: http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets """ - scheme = request.url.scheme - host = request.headers["host"] - - tms_list = morecantile.tms.list() return { "tileMatrixSets": [ { - "id": tms, - "title": morecantile.tms.get(tms).title, + "id": tms.name, + "title": morecantile.tms.get(tms.name).title, "links": [ { - "href": f"{scheme}://{host}/tileMatrixSets/{tms}", + "href": request.url_for( + "TileMatrixSet_info", TileMatrixSetId=tms.name + ), "rel": "item", "type": "application/json", } ], } - for tms in tms_list + for tms in TileMatrixSetNames ] } @@ -54,7 +52,7 @@ async def tms_list(request: Request): response_model_exclude_none=True, tags=["TileMatrixSets"], ) -async def tms_info( +async def TileMatrixSet_info( TileMatrixSetId: TileMatrixSetNames = Query(..., description="TileMatrixSet Name") ): """Return TileMatrixSet JSON document.""" diff --git a/titiler/models/cog.py b/titiler/models/cog.py index cb24daa40..8f9d38a3a 100644 --- a/titiler/models/cog.py +++ b/titiler/models/cog.py @@ -18,10 +18,9 @@ class cogBounds(BaseModel): bounds: BBox -class cogInfo(BaseModel): +class cogInfo(cogBounds): """COG Info.""" - bounds: Tuple[float, float, float, float] band_metadata: List[Tuple[int, Dict[int, Any]]] band_descriptions: List[Tuple[int, str]] dtype: str @@ -68,7 +67,7 @@ class CogeoInfoGeo(BaseModel): """rio-cogeo validation GEO information.""" CRS: str - BoundingBox: Tuple[float, float, float, float] + BoundingBox: BBox Origin: Tuple[float, float] Resolution: Tuple[float, float] diff --git a/titiler/templates/wmts.xml b/titiler/templates/wmts.xml index 02bcc0e9d..0cb225940 100644 --- a/titiler/templates/wmts.xml +++ b/titiler/templates/wmts.xml @@ -8,7 +8,7 @@ - + RESTful @@ -21,7 +21,7 @@ - + RESTful @@ -35,7 +35,7 @@ {{ title }} - cogeo + {{ layer_name }} {{ title }} {{ bounds[0] }} {{ bounds[1] }} @@ -48,7 +48,7 @@ {{ tms.identifier }} - + {{ tms.identifier }} @@ -58,5 +58,5 @@ {% endfor %} - + \ No newline at end of file