Skip to content

Commit 391d5d0

Browse files
add support for OGC common and Tiles (#1146)
1 parent 7b50a43 commit 391d5d0

File tree

18 files changed

+512
-41
lines changed

18 files changed

+512
-41
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111
### titiler.application
1212

1313
* fix Landing page links when app is behind proxy
14+
* use `titiler.core` templates for Landing page
15+
* enable JSON and HTML rendering of the `/` landing page
16+
* add OGC Common `/conformance` endpoint
1417

1518
### titiler.core
1619

20+
* add `conforms_to` attribute to `BaseFactory` to indicate which conformance the TileFactory implement
21+
1722
* remove deprecated `ColorFormulaParams` and `RescalingParams` dependencies **breaking change**
1823

1924
* remove deprecated `DefaultDependency` dict-unpacking feature **breaking change**

docs/src/advanced/endpoints_factories.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Most **Factories** are built from this [abstract based class](https://docs.pytho
2323
- **extension**: TiTiler extensions to register after endpoints creations. Defaults to `[]`.
2424
- **name**: Name of the Endpoints group. Defaults to `None`.
2525
- **operation_prefix** (*private*): Endpoint's `operationId` prefix. Defined by `self.name` or `self.router_prefix.replace("/", ".")`.
26+
- **conforms_to**: Set of conformance classes the Factory implement
2627

2728
#### Methods
2829

src/titiler/application/titiler/application/main.py

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import json
44
import logging
55
from logging import config as log_config
6+
from typing import Annotated, Literal, Optional
67

78
import jinja2
89
import rasterio
9-
from fastapi import Depends, FastAPI, HTTPException, Security
10+
from fastapi import Depends, FastAPI, HTTPException, Query, Security
1011
from fastapi.security.api_key import APIKeyQuery
1112
from rio_tiler.io import Reader, STACReader
1213
from starlette.middleware.cors import CORSMiddleware
1314
from starlette.requests import Request
14-
from starlette.responses import HTMLResponse
1515
from starlette.templating import Jinja2Templates
1616
from starlette_cramjam.middleware import CompressionMiddleware
1717

@@ -31,6 +31,10 @@
3131
LowerCaseQueryStringMiddleware,
3232
TotalTimeMiddleware,
3333
)
34+
from titiler.core.models.OGC import Conformance, Landing
35+
from titiler.core.resources.enums import MediaType
36+
from titiler.core.templating import create_html_response
37+
from titiler.core.utils import accept_media_type, update_openapi
3438
from titiler.extensions import (
3539
cogValidateExtension,
3640
cogViewerExtension,
@@ -97,6 +101,18 @@ def validate_access_token(access_token: str = Security(api_key_query)):
97101
dependencies=app_dependencies,
98102
)
99103

104+
# Fix OpenAPI response header for OGC Common compatibility
105+
update_openapi(app)
106+
107+
TITILER_CONFORMS_TO = {
108+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/core",
109+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page",
110+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30",
111+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/html",
112+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/json",
113+
}
114+
115+
100116
###############################################################################
101117
# Simple Dataset endpoints (e.g Cloud Optimized GeoTIFF)
102118
if not api_settings.disable_cog:
@@ -116,6 +132,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
116132
tags=["Cloud Optimized GeoTIFF"],
117133
)
118134

135+
TITILER_CONFORMS_TO.update(cog.conforms_to)
119136

120137
###############################################################################
121138
# STAC endpoints
@@ -135,6 +152,8 @@ def validate_access_token(access_token: str = Security(api_key_query)):
135152
tags=["SpatioTemporal Asset Catalog"],
136153
)
137154

155+
TITILER_CONFORMS_TO.update(stac.conforms_to)
156+
138157
###############################################################################
139158
# Mosaic endpoints
140159
if not api_settings.disable_mosaic:
@@ -145,13 +164,16 @@ def validate_access_token(access_token: str = Security(api_key_query)):
145164
tags=["MosaicJSON"],
146165
)
147166

167+
TITILER_CONFORMS_TO.update(mosaic.conforms_to)
168+
148169
###############################################################################
149170
# TileMatrixSets endpoints
150171
tms = TMSFactory()
151172
app.include_router(
152173
tms.router,
153174
tags=["Tiling Schemes"],
154175
)
176+
TITILER_CONFORMS_TO.update(tms.conforms_to)
155177

156178
###############################################################################
157179
# Algorithms endpoints
@@ -160,6 +182,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
160182
algorithms.router,
161183
tags=["Algorithms"],
162184
)
185+
TITILER_CONFORMS_TO.update(algorithms.conforms_to)
163186

164187
###############################################################################
165188
# Colormaps endpoints
@@ -168,6 +191,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
168191
cmaps.router,
169192
tags=["ColorMaps"],
170193
)
194+
TITILER_CONFORMS_TO.update(cmaps.conforms_to)
171195

172196

173197
add_exception_handlers(app, DEFAULT_STATUS_CODES)
@@ -289,30 +313,33 @@ def application_health_check():
289313
}
290314

291315

292-
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
293-
def landing(request: Request):
316+
@app.get(
317+
"/",
318+
response_model=Landing,
319+
response_model_exclude_none=True,
320+
responses={
321+
200: {
322+
"content": {
323+
"text/html": {},
324+
"application/json": {},
325+
}
326+
},
327+
},
328+
tags=["OGC Common"],
329+
)
330+
def landing(
331+
request: Request,
332+
f: Annotated[
333+
Optional[Literal["html", "json"]],
334+
Query(
335+
description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header."
336+
),
337+
] = None,
338+
):
294339
"""TiTiler landing page."""
295-
urlpath = request.url.path
296-
if root_path := request.scope.get("root_path"):
297-
urlpath = urlpath.removeprefix(root_path)
298-
299-
crumbs = []
300-
baseurl = str(request.base_url).rstrip("/")
301-
302-
crumbpath = str(baseurl)
303-
if urlpath == "/":
304-
urlpath = ""
305-
306-
for crumb in urlpath.split("/"):
307-
crumbpath = crumbpath.rstrip("/")
308-
part = crumb
309-
if part is None or part == "":
310-
part = "Home"
311-
crumbpath += f"/{crumb}"
312-
crumbs.append({"url": crumbpath.rstrip("/"), "part": part.capitalize()})
313-
314340
data = {
315-
"title": "titiler",
341+
"title": "TiTiler",
342+
"description": "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL.",
316343
"links": [
317344
{
318345
"title": "Landing page",
@@ -321,17 +348,23 @@ def landing(request: Request):
321348
"rel": "self",
322349
},
323350
{
324-
"title": "the API definition (JSON)",
351+
"title": "The API definition (JSON)",
325352
"href": str(request.url_for("openapi")),
326353
"type": "application/vnd.oai.openapi+json;version=3.0",
327354
"rel": "service-desc",
328355
},
329356
{
330-
"title": "the API documentation",
357+
"title": "The API documentation",
331358
"href": str(request.url_for("swagger_ui_html")),
332359
"type": "text/html",
333360
"rel": "service-doc",
334361
},
362+
{
363+
"title": "Conformance Declaration",
364+
"href": str(request.url_for("conformance")),
365+
"type": "text/html",
366+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/conformance",
367+
},
335368
{
336369
"title": "TiTiler Documentation (external link)",
337370
"href": "https://developmentseed.org/titiler/",
@@ -347,16 +380,74 @@ def landing(request: Request):
347380
],
348381
}
349382

350-
return templates.TemplateResponse(
351-
"index.html",
352-
{
353-
"request": request,
354-
"response": data,
355-
"template": {
356-
"api_root": baseurl,
357-
"params": request.query_params,
358-
"title": "TiTiler",
359-
},
360-
"crumbs": crumbs,
383+
output_type: Optional[MediaType]
384+
if f:
385+
output_type = MediaType[f]
386+
else:
387+
accepted_media = [MediaType.html, MediaType.json]
388+
output_type = accept_media_type(
389+
request.headers.get("accept", ""), accepted_media
390+
)
391+
392+
if output_type == MediaType.html:
393+
return create_html_response(
394+
request,
395+
data,
396+
title="TiTiler",
397+
template_name="landing",
398+
)
399+
400+
return data
401+
402+
403+
@app.get(
404+
"/conformance",
405+
response_model=Conformance,
406+
response_model_exclude_none=True,
407+
responses={
408+
200: {
409+
"content": {
410+
"text/html": {},
411+
"application/json": {},
412+
}
361413
},
362-
)
414+
},
415+
tags=["OGC Common"],
416+
)
417+
def conformance(
418+
request: Request,
419+
f: Annotated[
420+
Optional[Literal["html", "json"]],
421+
Query(
422+
description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header."
423+
),
424+
] = None,
425+
):
426+
"""Conformance classes.
427+
428+
Called with `GET /conformance`.
429+
430+
Returns:
431+
Conformance classes which the server conforms to.
432+
433+
"""
434+
data = {"conformsTo": sorted(TITILER_CONFORMS_TO)}
435+
436+
output_type: Optional[MediaType]
437+
if f:
438+
output_type = MediaType[f]
439+
else:
440+
accepted_media = [MediaType.html, MediaType.json]
441+
output_type = accept_media_type(
442+
request.headers.get("accept", ""), accepted_media
443+
)
444+
445+
if output_type == MediaType.html:
446+
return create_html_response(
447+
request,
448+
data,
449+
title="Conformance",
450+
template_name="conformance",
451+
)
452+
453+
return data

src/titiler/core/titiler/core/dependencies.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ class RenderingParams(DefaultDependency):
444444
),
445445
] = None
446446

447-
def __post_init__(self):
447+
def __post_init__(self) -> None:
448448
"""Post Init."""
449449
if self.rescale:
450450
rescale_array = []
@@ -460,7 +460,7 @@ def __post_init__(self):
460460
), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]"
461461
rescale_array.append(parsed)
462462

463-
self.rescale: RescaleType = rescale_array
463+
self.rescale: RescaleType = rescale_array # type: ignore
464464

465465

466466
@dataclass

src/titiler/core/titiler/core/factory.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Literal,
1010
Optional,
1111
Sequence,
12+
Set,
1213
Tuple,
1314
Type,
1415
Union,
@@ -149,6 +150,8 @@ class BaseFactory(metaclass=abc.ABCMeta):
149150
name: Optional[str] = field(default=None)
150151
operation_prefix: str = field(init=False, default="")
151152

153+
conforms_to: Set[str] = field(factory=set)
154+
152155
def __attrs_post_init__(self):
153156
"""Post Init: register route and configure specific options."""
154157
# prefix for endpoint's operationId
@@ -302,6 +305,20 @@ class TilerFactory(BaseFactory):
302305
add_part: bool = True
303306
add_viewer: bool = True
304307

308+
conforms_to: Set[str] = field(
309+
factory=lambda: {
310+
# https://docs.ogc.org/is/20-057/20-057.html#toc30
311+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tileset",
312+
# https://docs.ogc.org/is/20-057/20-057.html#toc34
313+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tilesets-list",
314+
# https://docs.ogc.org/is/20-057/20-057.html#toc65
315+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/core",
316+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/png",
317+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/jpeg",
318+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tiff",
319+
}
320+
)
321+
305322
def register_routes(self):
306323
"""
307324
This Method register routes to the router.

src/titiler/core/titiler/core/models/OGC.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,3 +730,25 @@ class TileSetList(BaseModel):
730730
"""
731731

732732
tilesets: List[TileSet]
733+
734+
735+
class Conformance(BaseModel):
736+
"""Conformance model.
737+
738+
Ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/confClasses.yaml
739+
740+
"""
741+
742+
conformsTo: List[str]
743+
744+
745+
class Landing(BaseModel):
746+
"""Landing page model.
747+
748+
Ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/landingPage.yaml
749+
750+
"""
751+
752+
title: Optional[str] = None
753+
description: Optional[str] = None
754+
links: List[Link]

0 commit comments

Comments
 (0)