Skip to content

Commit 5ed605c

Browse files
Merge pull request #173 from developmentseed/feature/new-multi-tms-viewer
(feature): new tile viewer supporting multiple TMS
2 parents a6c0038 + f93d777 commit 5ed605c

File tree

5 files changed

+265
-212
lines changed

5 files changed

+265
-212
lines changed

tests/test_factories.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def test_tiles_factory():
116116
endpoints = OGCTilesFactory()
117117
assert endpoints.with_common
118118
assert endpoints.title == "OGC API"
119-
assert len(endpoints.router.routes) == 14
119+
assert len(endpoints.router.routes) == 15
120120
assert len(endpoints.conforms_to) == 5
121121

122122
app = FastAPI()
@@ -127,7 +127,7 @@ def test_tiles_factory():
127127
assert response.headers["content-type"] == "application/json"
128128
assert response.json()["title"] == "OGC API"
129129
links = response.json()["links"]
130-
assert len(links) == 9 # 5 from tiles + 4 from common
130+
assert len(links) == 10 # 6 from tiles + 4 from common
131131
landing_link = [link for link in links if link["title"] == "Landing Page"][0]
132132
assert landing_link["href"] == "http://testserver/"
133133
tms_link = [link for link in links if link["title"] == "TileMatrixSets"][0]
@@ -150,7 +150,7 @@ def test_tiles_factory():
150150
assert endpoints.router_prefix == "/map"
151151
assert endpoints.with_common
152152
assert endpoints.title == "OGC Tiles API"
153-
assert len(endpoints.router.routes) == 14
153+
assert len(endpoints.router.routes) == 15
154154

155155
app = FastAPI()
156156
app.include_router(endpoints.router, prefix="/map")
@@ -160,7 +160,7 @@ def test_tiles_factory():
160160
assert response.headers["content-type"] == "application/json"
161161
assert response.json()["title"] == "OGC Tiles API"
162162
links = response.json()["links"]
163-
assert len(links) == 9
163+
assert len(links) == 10
164164
landing_link = [link for link in links if link["title"] == "Landing Page"][0]
165165
assert landing_link["href"] == "http://testserver/map/"
166166
tms_link = [link for link in links if link["title"] == "TileMatrixSets"][0]
@@ -180,7 +180,7 @@ def test_tiles_factory():
180180
endpoints = OGCTilesFactory(title="OGC Tiles API", with_common=False)
181181
assert not endpoints.with_common
182182
assert endpoints.title == "OGC Tiles API"
183-
assert len(endpoints.router.routes) == 12
183+
assert len(endpoints.router.routes) == 13
184184
assert len(endpoints.conforms_to) == 5
185185

186186
app = FastAPI()
@@ -203,7 +203,7 @@ def test_endpoints_factory():
203203
endpoints = Endpoints()
204204
assert endpoints.with_common
205205
assert endpoints.title == "OGC API"
206-
assert len(endpoints.router.routes) == 19
206+
assert len(endpoints.router.routes) == 20
207207
assert len(endpoints.conforms_to) == 11 # 5 from tiles + 6 from features
208208

209209
app = FastAPI()
@@ -214,7 +214,7 @@ def test_endpoints_factory():
214214
assert response.headers["content-type"] == "application/json"
215215
assert response.json()["title"] == "OGC API"
216216
links = response.json()["links"]
217-
assert len(links) == 14 # 5 from tiles + 5 from features + 4 from common
217+
assert len(links) == 15 # 6 from tiles + 5 from features + 4 from common
218218
landing_link = [link for link in links if link["title"] == "Landing Page"][0]
219219
assert landing_link["href"] == "http://testserver/"
220220
queryables_link = [
@@ -237,14 +237,14 @@ def test_endpoints_factory():
237237
assert response.status_code == 200
238238
assert response.headers["content-type"] == "application/json"
239239
body = response.json()["conformsTo"]
240-
assert len(body) > 10 # 4 from tiles + 6 from features
240+
assert len(body) > 9 # 3 from tiles + 6 from features
241241
assert "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core" in body
242242

243243
endpoints = Endpoints(router_prefix="/ogc", title="OGC Full API", with_common=True)
244244
assert endpoints.router_prefix == "/ogc"
245245
assert endpoints.with_common
246246
assert endpoints.title == "OGC Full API"
247-
assert len(endpoints.router.routes) == 19
247+
assert len(endpoints.router.routes) == 20
248248
assert not endpoints.ogc_features.with_common
249249
assert endpoints.ogc_features.router_prefix == "/ogc"
250250
assert not endpoints.ogc_tiles.with_common
@@ -258,7 +258,7 @@ def test_endpoints_factory():
258258
assert response.headers["content-type"] == "application/json"
259259
assert response.json()["title"] == "OGC Full API"
260260
links = response.json()["links"]
261-
assert len(links) == 14
261+
assert len(links) == 15
262262
landing_link = [link for link in links if link["title"] == "Landing Page"][0]
263263
assert landing_link["href"] == "http://testserver/ogc/"
264264
queryables_link = [
@@ -288,7 +288,7 @@ def test_endpoints_factory():
288288
endpoints = Endpoints(title="Tiles and Features API", with_common=False)
289289
assert not endpoints.with_common
290290
assert endpoints.title == "Tiles and Features API"
291-
assert len(endpoints.router.routes) == 17 # 10 from tiles + 5 from features
291+
assert len(endpoints.router.routes) == 18 # 11 from tiles + 5 from features
292292
assert len(endpoints.conforms_to) == 11 # 4 from tiles + 6 from features
293293

294294
app = FastAPI()

tipg/dependencies.py

-2
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,6 @@ def TileParams(
318318
z: Annotated[
319319
int,
320320
Path(
321-
ge=0,
322-
le=30,
323321
description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.",
324322
),
325323
],

tipg/factory.py

+103-48
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,10 @@ class EndpointsFactory(metaclass=abc.ABCMeta):
187187

188188
def __post_init__(self):
189189
"""Post Init: register route and configure specific options."""
190-
self.register_routes()
191190
if self.with_common:
192-
self._conformance_route()
193191
self._landing_route()
192+
self._conformance_route()
193+
self.register_routes()
194194

195195
def url_for(self, request: Request, name: str, **path_params: Any) -> str:
196196
"""Return full url (with prefix) for a specific handler."""
@@ -1111,7 +1111,7 @@ def conforms_to(self) -> List[str]:
11111111

11121112
def links(self, request: Request) -> List[model.Link]:
11131113
"""OGC Tiles API links."""
1114-
return [
1114+
links = [
11151115
model.Link(
11161116
title="Collection Vector Tiles (Template URL)",
11171117
href=self.url_for(
@@ -1149,6 +1149,25 @@ def links(self, request: Request) -> List[model.Link]:
11491149
rel="data",
11501150
templated=True,
11511151
),
1152+
]
1153+
1154+
if self.with_viewer:
1155+
links.append(
1156+
model.Link(
1157+
title="Collection Map viewer (Template URL)",
1158+
href=self.url_for(
1159+
request,
1160+
"viewer_endpoint",
1161+
collectionId="{collectionId}",
1162+
tileMatrixSetId="{tileMatrixSetId}",
1163+
),
1164+
type=MediaType.html,
1165+
rel="data",
1166+
templated=True,
1167+
)
1168+
)
1169+
1170+
links += [
11521171
model.Link(
11531172
title="TileMatrixSets",
11541173
href=self.url_for(
@@ -1171,6 +1190,8 @@ def links(self, request: Request) -> List[model.Link]:
11711190
),
11721191
]
11731192

1193+
return links
1194+
11741195
def register_routes(self): # noqa: C901
11751196
"""Register OGC Tiles endpoints."""
11761197
self._tilematrixsets_routes()
@@ -1428,49 +1449,67 @@ async def collection_tileset(
14281449
for matrix in tms
14291450
]
14301451

1452+
links = [
1453+
{
1454+
"href": self.url_for(
1455+
request,
1456+
"collection_tileset",
1457+
collectionId=collection.id,
1458+
tileMatrixSetId=tileMatrixSetId,
1459+
),
1460+
"rel": "self",
1461+
"type": "application/json",
1462+
"title": f"'{collection.id}' tileset tiled using {tileMatrixSetId} TileMatrixSet",
1463+
},
1464+
{
1465+
"href": self.url_for(
1466+
request,
1467+
"tilematrixset",
1468+
tileMatrixSetId=tileMatrixSetId,
1469+
),
1470+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes",
1471+
"type": "application/json",
1472+
"title": f"Definition of '{tileMatrixSetId}' tileMatrixSet",
1473+
},
1474+
{
1475+
"href": self.url_for(
1476+
request,
1477+
"collection_get_tile",
1478+
tileMatrixSetId=tileMatrixSetId,
1479+
collectionId=collection.id,
1480+
z="{z}",
1481+
x="{x}",
1482+
y="{y}",
1483+
),
1484+
"rel": "tile",
1485+
"type": "application/vnd.mapbox-vector-tile",
1486+
"title": "Templated link for retrieving Vector tiles",
1487+
"templated": True,
1488+
},
1489+
]
1490+
1491+
if self.with_viewer:
1492+
links.append(
1493+
{
1494+
"href": self.url_for(
1495+
request,
1496+
"viewer_endpoint",
1497+
tileMatrixSetId=tileMatrixSetId,
1498+
collectionId=collection.id,
1499+
),
1500+
"type": "text/html",
1501+
"rel": "data",
1502+
"title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet",
1503+
}
1504+
)
1505+
14311506
data = model.TileSet.model_validate(
14321507
{
14331508
"title": f"'{collection.id}' tileset tiled using {tileMatrixSetId} TileMatrixSet",
14341509
"dataType": "vector",
14351510
"crs": tms.crs,
14361511
"boundingBox": collection_bbox,
1437-
"links": [
1438-
{
1439-
"href": self.url_for(
1440-
request,
1441-
"collection_tileset",
1442-
collectionId=collection.id,
1443-
tileMatrixSetId=tileMatrixSetId,
1444-
),
1445-
"rel": "self",
1446-
"type": "application/json",
1447-
"title": f"'{collection.id}' tileset tiled using {tileMatrixSetId} TileMatrixSet",
1448-
},
1449-
{
1450-
"href": self.url_for(
1451-
request,
1452-
"tilematrixset",
1453-
tileMatrixSetId=tileMatrixSetId,
1454-
),
1455-
"rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes",
1456-
"type": "application/json",
1457-
"title": f"Definition of '{tileMatrixSetId}' tileMatrixSet",
1458-
},
1459-
{
1460-
"href": self.url_for(
1461-
request,
1462-
"collection_get_tile",
1463-
tileMatrixSetId=tileMatrixSetId,
1464-
collectionId=collection.id,
1465-
z="{z}",
1466-
x="{x}",
1467-
y="{y}",
1468-
),
1469-
"rel": "tile",
1470-
"type": "application/vnd.mapbox-vector-tile",
1471-
"title": "Templated link for retrieving Vector tiles",
1472-
},
1473-
],
1512+
"links": links,
14741513
"tileMatrixSetLimits": tilematrix_limit,
14751514
}
14761515
)
@@ -1499,6 +1538,7 @@ def _tile_routes(self):
14991538
responses={200: {"content": {MediaType.mvt.value: {}}}},
15001539
operation_id=".collection.vector.getTile",
15011540
tags=["OGC Tiles API"],
1541+
deprecated=True,
15021542
)
15031543
async def collection_get_tile(
15041544
request: Request,
@@ -1582,6 +1622,7 @@ def _tilejson_routes(self):
15821622
response_class=ORJSONResponse,
15831623
operation_id=".collection.vector.getTileJSON",
15841624
tags=["OGC Tiles API"],
1625+
deprecated=True,
15851626
)
15861627
async def collection_tilejson(
15871628
request: Request,
@@ -1683,6 +1724,7 @@ def _stylejson_routes(self):
16831724
response_class=ORJSONResponse,
16841725
operation_id=".collection.vector.getStyleJSON",
16851726
tags=["OGC Tiles API"],
1727+
deprecated=True,
16861728
)
16871729
async def collection_stylejson(
16881730
request: Request,
@@ -1809,19 +1851,29 @@ async def collection_stylejson(
18091851
"/collections/{collectionId}/{tileMatrixSetId}/viewer",
18101852
response_class=HTMLResponse,
18111853
operation_id=".collection.vector.viewerTms",
1854+
deprecated=True,
1855+
tags=["Map Viewer"],
18121856
)
18131857
@self.router.get(
18141858
"/collections/{collectionId}/viewer",
18151859
response_class=HTMLResponse,
18161860
operation_id=".collection.vector.viewer",
1861+
deprecated=True,
1862+
tags=["Map Viewer"],
1863+
)
1864+
@self.router.get(
1865+
"/collections/{collectionId}/tiles/{tileMatrixSetId}/viewer",
1866+
response_class=HTMLResponse,
1867+
operation_id=".collection.vector.map",
1868+
tags=["Map Viewer"],
18171869
)
18181870
def viewer_endpoint(
18191871
request: Request,
18201872
collection: Annotated[Collection, Depends(self.collection_dependency)],
18211873
tileMatrixSetId: Annotated[
1822-
Literal["WebMercatorQuad"],
1823-
"Identifier selecting one of the TileMatrixSetId supported (default: 'WebMercatorQuad')",
1824-
] = "WebMercatorQuad",
1874+
Literal[tuple(self.supported_tms.list())],
1875+
f"Identifier selecting one of the TileMatrixSetId supported (default: '{tms_settings.default_tms}')",
1876+
] = tms_settings.default_tms,
18251877
minzoom: Annotated[
18261878
Optional[int],
18271879
Query(description="Overwrite default minzoom."),
@@ -1839,7 +1891,7 @@ def viewer_endpoint(
18391891
] = None,
18401892
):
18411893
"""Return Simple HTML Viewer for a collection."""
1842-
self.supported_tms.get(tileMatrixSetId)
1894+
tms = self.supported_tms.get(tileMatrixSetId)
18431895

18441896
tilejson_url = self.url_for(
18451897
request,
@@ -1850,13 +1902,16 @@ def viewer_endpoint(
18501902
if request.query_params._list:
18511903
tilejson_url += f"?{urlencode(request.query_params._list)}"
18521904

1853-
return self.templates.TemplateResponse(
1905+
return self._create_html_response(
18541906
request,
1855-
name="map.html",
1856-
context={
1907+
{
1908+
"title": collection.id,
18571909
"tilejson_endpoint": tilejson_url,
1910+
"tms": tms,
1911+
"resolutions": [matrix.cellSize for matrix in tms],
18581912
},
1859-
media_type="text/html",
1913+
template_name="map",
1914+
title=f"{collection.id} viewer",
18601915
)
18611916

18621917

tipg/model.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ class TileJSON(BaseModel):
307307
vector_layers: Optional[List[LayerJSON]] = None
308308
grids: Optional[List[str]] = None
309309
data: Optional[List[str]] = None
310-
minzoom: int = Field(0, ge=0, le=30)
311-
maxzoom: int = Field(30, ge=0, le=30)
310+
minzoom: int = Field(0)
311+
maxzoom: int = Field(30)
312312
fillzoom: Optional[int] = None
313313
bounds: List[float] = [180, -85.05112877980659, 180, 85.0511287798066]
314314
center: Optional[Tuple[float, float, int]] = None

0 commit comments

Comments
 (0)