diff --git a/tests/test_factories.py b/tests/test_factories.py index 35dd407..c23ec73 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -116,7 +116,7 @@ def test_tiles_factory(): endpoints = OGCTilesFactory() assert endpoints.with_common assert endpoints.title == "OGC API" - assert len(endpoints.router.routes) == 14 + assert len(endpoints.router.routes) == 15 assert len(endpoints.conforms_to) == 5 app = FastAPI() @@ -127,7 +127,7 @@ def test_tiles_factory(): assert response.headers["content-type"] == "application/json" assert response.json()["title"] == "OGC API" links = response.json()["links"] - assert len(links) == 9 # 5 from tiles + 4 from common + assert len(links) == 10 # 6 from tiles + 4 from common landing_link = [link for link in links if link["title"] == "Landing Page"][0] assert landing_link["href"] == "http://testserver/" tms_link = [link for link in links if link["title"] == "TileMatrixSets"][0] @@ -150,7 +150,7 @@ def test_tiles_factory(): assert endpoints.router_prefix == "/map" assert endpoints.with_common assert endpoints.title == "OGC Tiles API" - assert len(endpoints.router.routes) == 14 + assert len(endpoints.router.routes) == 15 app = FastAPI() app.include_router(endpoints.router, prefix="/map") @@ -160,7 +160,7 @@ def test_tiles_factory(): assert response.headers["content-type"] == "application/json" assert response.json()["title"] == "OGC Tiles API" links = response.json()["links"] - assert len(links) == 9 + assert len(links) == 10 landing_link = [link for link in links if link["title"] == "Landing Page"][0] assert landing_link["href"] == "http://testserver/map/" tms_link = [link for link in links if link["title"] == "TileMatrixSets"][0] @@ -180,7 +180,7 @@ def test_tiles_factory(): endpoints = OGCTilesFactory(title="OGC Tiles API", with_common=False) assert not endpoints.with_common assert endpoints.title == "OGC Tiles API" - assert len(endpoints.router.routes) == 12 + assert len(endpoints.router.routes) == 13 assert len(endpoints.conforms_to) == 5 app = FastAPI() @@ -203,7 +203,7 @@ def test_endpoints_factory(): endpoints = Endpoints() assert endpoints.with_common assert endpoints.title == "OGC API" - assert len(endpoints.router.routes) == 19 + assert len(endpoints.router.routes) == 20 assert len(endpoints.conforms_to) == 11 # 5 from tiles + 6 from features app = FastAPI() @@ -214,7 +214,7 @@ def test_endpoints_factory(): assert response.headers["content-type"] == "application/json" assert response.json()["title"] == "OGC API" links = response.json()["links"] - assert len(links) == 14 # 5 from tiles + 5 from features + 4 from common + assert len(links) == 15 # 6 from tiles + 5 from features + 4 from common landing_link = [link for link in links if link["title"] == "Landing Page"][0] assert landing_link["href"] == "http://testserver/" queryables_link = [ @@ -237,14 +237,14 @@ def test_endpoints_factory(): assert response.status_code == 200 assert response.headers["content-type"] == "application/json" body = response.json()["conformsTo"] - assert len(body) > 10 # 4 from tiles + 6 from features + assert len(body) > 9 # 3 from tiles + 6 from features assert "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core" in body endpoints = Endpoints(router_prefix="/ogc", title="OGC Full API", with_common=True) assert endpoints.router_prefix == "/ogc" assert endpoints.with_common assert endpoints.title == "OGC Full API" - assert len(endpoints.router.routes) == 19 + assert len(endpoints.router.routes) == 20 assert not endpoints.ogc_features.with_common assert endpoints.ogc_features.router_prefix == "/ogc" assert not endpoints.ogc_tiles.with_common @@ -258,7 +258,7 @@ def test_endpoints_factory(): assert response.headers["content-type"] == "application/json" assert response.json()["title"] == "OGC Full API" links = response.json()["links"] - assert len(links) == 14 + assert len(links) == 15 landing_link = [link for link in links if link["title"] == "Landing Page"][0] assert landing_link["href"] == "http://testserver/ogc/" queryables_link = [ @@ -288,7 +288,7 @@ def test_endpoints_factory(): endpoints = Endpoints(title="Tiles and Features API", with_common=False) assert not endpoints.with_common assert endpoints.title == "Tiles and Features API" - assert len(endpoints.router.routes) == 17 # 10 from tiles + 5 from features + assert len(endpoints.router.routes) == 18 # 11 from tiles + 5 from features assert len(endpoints.conforms_to) == 11 # 4 from tiles + 6 from features app = FastAPI() diff --git a/tipg/dependencies.py b/tipg/dependencies.py index 63a2562..4b5320f 100644 --- a/tipg/dependencies.py +++ b/tipg/dependencies.py @@ -318,8 +318,6 @@ def TileParams( z: Annotated[ int, Path( - ge=0, - le=30, description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", ), ], diff --git a/tipg/factory.py b/tipg/factory.py index ec68139..de7efa7 100644 --- a/tipg/factory.py +++ b/tipg/factory.py @@ -187,10 +187,10 @@ class EndpointsFactory(metaclass=abc.ABCMeta): def __post_init__(self): """Post Init: register route and configure specific options.""" - self.register_routes() if self.with_common: - self._conformance_route() self._landing_route() + self._conformance_route() + self.register_routes() def url_for(self, request: Request, name: str, **path_params: Any) -> str: """Return full url (with prefix) for a specific handler.""" @@ -1111,7 +1111,7 @@ def conforms_to(self) -> List[str]: def links(self, request: Request) -> List[model.Link]: """OGC Tiles API links.""" - return [ + links = [ model.Link( title="Collection Vector Tiles (Template URL)", href=self.url_for( @@ -1149,6 +1149,25 @@ def links(self, request: Request) -> List[model.Link]: rel="data", templated=True, ), + ] + + if self.with_viewer: + links.append( + model.Link( + title="Collection Map viewer (Template URL)", + href=self.url_for( + request, + "viewer_endpoint", + collectionId="{collectionId}", + tileMatrixSetId="{tileMatrixSetId}", + ), + type=MediaType.html, + rel="data", + templated=True, + ) + ) + + links += [ model.Link( title="TileMatrixSets", href=self.url_for( @@ -1171,6 +1190,8 @@ def links(self, request: Request) -> List[model.Link]: ), ] + return links + def register_routes(self): # noqa: C901 """Register OGC Tiles endpoints.""" self._tilematrixsets_routes() @@ -1428,49 +1449,67 @@ async def collection_tileset( for matrix in tms ] + links = [ + { + "href": self.url_for( + request, + "collection_tileset", + collectionId=collection.id, + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "self", + "type": "application/json", + "title": f"'{collection.id}' tileset tiled using {tileMatrixSetId} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tilematrixset", + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + "type": "application/json", + "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", + }, + { + "href": self.url_for( + request, + "collection_get_tile", + tileMatrixSetId=tileMatrixSetId, + collectionId=collection.id, + z="{z}", + x="{x}", + y="{y}", + ), + "rel": "tile", + "type": "application/vnd.mapbox-vector-tile", + "title": "Templated link for retrieving Vector tiles", + "templated": True, + }, + ] + + if self.with_viewer: + links.append( + { + "href": self.url_for( + request, + "viewer_endpoint", + tileMatrixSetId=tileMatrixSetId, + collectionId=collection.id, + ), + "type": "text/html", + "rel": "data", + "title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet", + } + ) + data = model.TileSet.model_validate( { "title": f"'{collection.id}' tileset tiled using {tileMatrixSetId} TileMatrixSet", "dataType": "vector", "crs": tms.crs, "boundingBox": collection_bbox, - "links": [ - { - "href": self.url_for( - request, - "collection_tileset", - collectionId=collection.id, - tileMatrixSetId=tileMatrixSetId, - ), - "rel": "self", - "type": "application/json", - "title": f"'{collection.id}' tileset tiled using {tileMatrixSetId} TileMatrixSet", - }, - { - "href": self.url_for( - request, - "tilematrixset", - tileMatrixSetId=tileMatrixSetId, - ), - "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", - "type": "application/json", - "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", - }, - { - "href": self.url_for( - request, - "collection_get_tile", - tileMatrixSetId=tileMatrixSetId, - collectionId=collection.id, - z="{z}", - x="{x}", - y="{y}", - ), - "rel": "tile", - "type": "application/vnd.mapbox-vector-tile", - "title": "Templated link for retrieving Vector tiles", - }, - ], + "links": links, "tileMatrixSetLimits": tilematrix_limit, } ) @@ -1499,6 +1538,7 @@ def _tile_routes(self): responses={200: {"content": {MediaType.mvt.value: {}}}}, operation_id=".collection.vector.getTile", tags=["OGC Tiles API"], + deprecated=True, ) async def collection_get_tile( request: Request, @@ -1582,6 +1622,7 @@ def _tilejson_routes(self): response_class=ORJSONResponse, operation_id=".collection.vector.getTileJSON", tags=["OGC Tiles API"], + deprecated=True, ) async def collection_tilejson( request: Request, @@ -1683,6 +1724,7 @@ def _stylejson_routes(self): response_class=ORJSONResponse, operation_id=".collection.vector.getStyleJSON", tags=["OGC Tiles API"], + deprecated=True, ) async def collection_stylejson( request: Request, @@ -1809,19 +1851,29 @@ async def collection_stylejson( "/collections/{collectionId}/{tileMatrixSetId}/viewer", response_class=HTMLResponse, operation_id=".collection.vector.viewerTms", + deprecated=True, + tags=["Map Viewer"], ) @self.router.get( "/collections/{collectionId}/viewer", response_class=HTMLResponse, operation_id=".collection.vector.viewer", + deprecated=True, + tags=["Map Viewer"], + ) + @self.router.get( + "/collections/{collectionId}/tiles/{tileMatrixSetId}/viewer", + response_class=HTMLResponse, + operation_id=".collection.vector.map", + tags=["Map Viewer"], ) def viewer_endpoint( request: Request, collection: Annotated[Collection, Depends(self.collection_dependency)], tileMatrixSetId: Annotated[ - Literal["WebMercatorQuad"], - "Identifier selecting one of the TileMatrixSetId supported (default: 'WebMercatorQuad')", - ] = "WebMercatorQuad", + Literal[tuple(self.supported_tms.list())], + f"Identifier selecting one of the TileMatrixSetId supported (default: '{tms_settings.default_tms}')", + ] = tms_settings.default_tms, minzoom: Annotated[ Optional[int], Query(description="Overwrite default minzoom."), @@ -1839,7 +1891,7 @@ def viewer_endpoint( ] = None, ): """Return Simple HTML Viewer for a collection.""" - self.supported_tms.get(tileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) tilejson_url = self.url_for( request, @@ -1850,13 +1902,16 @@ def viewer_endpoint( if request.query_params._list: tilejson_url += f"?{urlencode(request.query_params._list)}" - return self.templates.TemplateResponse( + return self._create_html_response( request, - name="map.html", - context={ + { + "title": collection.id, "tilejson_endpoint": tilejson_url, + "tms": tms, + "resolutions": [matrix.cellSize for matrix in tms], }, - media_type="text/html", + template_name="map", + title=f"{collection.id} viewer", ) diff --git a/tipg/model.py b/tipg/model.py index bed3f9d..442e49b 100644 --- a/tipg/model.py +++ b/tipg/model.py @@ -307,8 +307,8 @@ class TileJSON(BaseModel): vector_layers: Optional[List[LayerJSON]] = None grids: Optional[List[str]] = None data: Optional[List[str]] = None - minzoom: int = Field(0, ge=0, le=30) - maxzoom: int = Field(30, ge=0, le=30) + minzoom: int = Field(0) + maxzoom: int = Field(30) fillzoom: Optional[int] = None bounds: List[float] = [180, -85.05112877980659, 180, 85.0511287798066] center: Optional[Tuple[float, float, int]] = None diff --git a/tipg/templates/map.html b/tipg/templates/map.html index 511d3fc..b730af4 100644 --- a/tipg/templates/map.html +++ b/tipg/templates/map.html @@ -1,153 +1,153 @@ - - -
- -