diff --git a/CHANGES.md b/CHANGES.md index 0afe1c1..33d93df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,9 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin ## [unreleased] * switch to official python docker image from `bitnami` +* add `FactoryExtension` to `EndpointsFactory` +* rename `viewer_endpoint` function to `map_viewer` **breaking change** +* rename `/viewer` endpoint to `/map.html` **breaking change** ## [1.2.1] - 2025-08-26 diff --git a/docs/src/user_guide/endpoints.md b/docs/src/user_guide/endpoints.md index d72cb34..b3c6cdd 100644 --- a/docs/src/user_guide/endpoints.md +++ b/docs/src/user_guide/endpoints.md @@ -657,7 +657,7 @@ Return a TileJSON document. **Not in OGC Tile API specification** Path: -- `/collections/{collectionId}/{tileMatrixSetId}/tilejson.json` +- `/collections/{collectionId}/tiles/{tileMatrixSetId}/tilejson.json` PathParams: @@ -677,7 +677,7 @@ QueryParams: Example: ```json -curl http://127.0.0.1:8081/collections/public.landsat_wrs/WebMercatorQuad/tilejson.json | jq +curl http://127.0.0.1:8081/collections/public.landsat_wrs/tiles/WebMercatorQuad/tilejson.json | jq { "tilejson": "3.0.0", "name": "public.landsat_wrs", @@ -722,7 +722,7 @@ Return a mapbox/maplibre StyleJSON document. **Not in OGC Tile API specification Path: -- `/collections/{collectionId}/{tileMatrixSetId}/style.json` +- `/collections/{collectionId}/tiles/{tileMatrixSetId}/style.json` PathParams: @@ -741,7 +741,7 @@ QueryParams: \* **Not in OGC API Tiles Specification** ```json -// http://127.0.0.1:8081/collections/public.landsat_wrs/WebMercatorQuad/style.json +// http://127.0.0.1:8081/collections/public.landsat_wrs/tiles/WebMercatorQuad/style.json { "version": 8, "name": "TiPg", @@ -831,7 +831,7 @@ Simple Map viewer. **Not in OGC Tile API specification** Path: -- `/collections/{collectionId}/{tileMatrixSetId}/viewer` +- `/collections/{collectionId}/tiles/{tileMatrixSetId}/map.html` PathParams: diff --git a/docs/src/user_guide/factories.md b/docs/src/user_guide/factories.md index 6cd75c7..e71f3a9 100644 --- a/docs/src/user_guide/factories.md +++ b/docs/src/user_guide/factories.md @@ -115,7 +115,7 @@ app.include_router(endpoints.router) - **supported_tms** (morecantile.TileMatrixSets): morecantile TileMatrixSets instance (holds a set of TileMatrixSet documents) -- **with_viewer** (bool, optional): add `/viewer` endpoint to visualize the Vector tile. Defaults to `True` +- **with_map_viewer** (bool, optional): add `/map.html` endpoint to visualize the Vector tile. Defaults to `True` - **with_common** (bool, optional): Create Full OGC Features API set of endpoints with OGC Common endpoints (landing `/` and conformance `/conformance`). Defaults to `True` @@ -134,9 +134,9 @@ app.include_router(endpoints.router) | `GET` | `/collections/{collectionId}/tiles` | JSON | list of available vector tilesets | `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}` | JSON | vector tileset metadata | `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/{z}/{x}/{y}` | Mapbox Vector Tile (Protobuf) | create a web map vector tile from collection's items -| `GET` | `/collections/{collectionId}/{tileMatrixSetId}/tilejson.json` | JSON | Mapbox TileJSON document -| `GET` | `/collections/{collectionId}/{tileMatrixSetId}/style.json` | JSON | Mapbox/Maplibre StyleJSON document -| `GET` | `/collections/{collectionId}/{tileMatrixSetId}/viewer` | HTML | simple map viewer **[OPTIONAL]** +| `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/tilejson.json` | JSON | Mapbox TileJSON document +| `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/style.json` | JSON | Mapbox/Maplibre StyleJSON document +| `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/map.html` | HTML | simple map viewer **[OPTIONAL]** | `GET` | `/tileMatrixSets` | JSON | list of available TileMatrixSets | `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON | TileMatrixSet document | `GET` | `/conformance` | HTML / JSON | conformance class landing Page @@ -160,7 +160,7 @@ app.include_router(endpoints.router) - **supported_tms** (morecantile.TileMatrixSets): morecantile TileMatrixSets instance (holds a set of TileMatrixSet documents) -- **with_tiles_viewer** (bool, optional): add `/viewer` endpoint to visualize the Vector tile. Defaults to `True` +- **with_tiles_viewer** (bool, optional): add `/map.html` endpoint to visualize the Vector tile. Defaults to `True` - **with_common** (bool, optional): Create Full OGC Features API set of endpoints with OGC Common endpoints (landing `/` and conformance `/conformance`). Defaults to `True` @@ -184,9 +184,9 @@ app.include_router(endpoints.router) | `GET` | `/collections/{collectionId}/tiles` | JSON | list of available vector tilesets | `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}` | JSON | vector tileset metadata | `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/{z}/{x}/{y}` | Mapbox Vector Tile (Protobuf) | create a web map vector tile from collection's items -| `GET` | `/collections/{collectionId}/{tileMatrixSetId}/tilejson.json` | JSON | Mapbox TileJSON document -| `GET` | `/collections/{collectionId}/{tileMatrixSetId}/style.json` | JSON | Mapbox/Maplibre StyleJSON document -| `GET` | `/collections/{collectionId}/{tileMatrixSetId}/viewer` | HTML | simple map viewer **[OPTIONAL]** +| `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/tilejson.json` | JSON | Mapbox TileJSON document +| `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/style.json` | JSON | Mapbox/Maplibre StyleJSON document +| `GET` | `/collections/{collectionId}/tiles/{tileMatrixSetId}/map.html` | HTML | simple map viewer **[OPTIONAL]** | `GET` | `/tileMatrixSets` | JSON | list of available TileMatrixSets | `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON | TileMatrixSet document | `GET` | `/conformance` | HTML / JSON | conformance class landing Page diff --git a/tipg/extensions/__init__.py b/tipg/extensions/__init__.py new file mode 100644 index 0000000..271f592 --- /dev/null +++ b/tipg/extensions/__init__.py @@ -0,0 +1 @@ +"""Tipg extensions""" diff --git a/tipg/extensions/viewer.py b/tipg/extensions/viewer.py new file mode 100644 index 0000000..77cc9fd --- /dev/null +++ b/tipg/extensions/viewer.py @@ -0,0 +1,66 @@ +"""viewer Extension.""" + +from dataclasses import dataclass +from typing import Annotated, Optional +from urllib.parse import urlencode + +from tipg.collections import Collection +from tipg.factory import EndpointsFactory, FactoryExtension + +from fastapi import Depends, Query + +from starlette.requests import Request +from starlette.responses import HTMLResponse + + +@dataclass +class viewerExtension(FactoryExtension): + """Add /validate endpoint to a COG TilerFactory.""" + + def register(self, factory: EndpointsFactory): + """Register endpoint to the tiler factory.""" + + @factory.router.get( + "/collections/{collectionId}/viewer.html", + response_class=HTMLResponse, + operation_id=".collection.vector.viewer", + tags=["Map Viewer"], + ) + def viewer( + request: Request, + collection: Annotated[Collection, Depends(factory.collection_dependency)], + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + geom_column: Annotated[ + Optional[str], + Query( + description="Select geometry column.", + alias="geom-column", + ), + ] = None, + ): + """Return Simple HTML Viewer for a collection.""" + stylejson_url = factory.url_for( + request, + "collection_stylejson", + collectionId=collection.id, + tileMatrixSetId="WebMercatorQuad", + ) + if request.query_params._list: + stylejson_url += f"?{urlencode(request.query_params._list)}" + + return factory._create_html_response( + request, + { + "title": collection.id, + "stylejson_endpoint": stylejson_url, + }, + template_name="viewer", + title=f"{collection.id} viewer", + ) diff --git a/tipg/factory.py b/tipg/factory.py index cf07281..3959ffe 100644 --- a/tipg/factory.py +++ b/tipg/factory.py @@ -176,6 +176,16 @@ def create_html_response( ) +@dataclass +class FactoryExtension(metaclass=abc.ABCMeta): + """Factory Extension.""" + + @abc.abstractmethod + def register(self, factory: "EndpointsFactory"): + """Register extension to the factory.""" + ... + + # ref: https://github.com/python/mypy/issues/5374 @dataclass # type: ignore class EndpointsFactory(metaclass=abc.ABCMeta): @@ -191,6 +201,8 @@ class EndpointsFactory(metaclass=abc.ABCMeta): # e.g if you mount the route with `/foo` prefix, set router_prefix to foo router_prefix: str = "" + extensions: List[FactoryExtension] = field(default_factory=list) + templates: Jinja2Templates = DEFAULT_TEMPLATES # Full application with Landing and Conformance @@ -203,8 +215,13 @@ def __post_init__(self): if self.with_common: self._landing_route() self._conformance_route() + self.register_routes() + # Register Extensions + for ext in self.extensions: + ext.register(self) + def url_for(self, request: Request, name: str, **path_params: Any) -> str: """Return full url (with prefix) for a specific handler.""" url_path = self.router.url_path_for(name, **path_params) @@ -463,7 +480,7 @@ def _additional_collection_tiles_links( title="Collection Map viewer (Template URL)", href=str( request.app.url_path_for( - "viewer_endpoint", + "map_viewer", collectionId=collection.id, tileMatrixSetId="{tileMatrixSetId}", ).make_absolute_url(base_url=base_url) @@ -1238,7 +1255,7 @@ def links(self, request: Request) -> List[model.Link]: title="Collection Map viewer (Template URL)", href=self.url_for( request, - "viewer_endpoint", + "map_viewer", collectionId="{collectionId}", tileMatrixSetId="{tileMatrixSetId}", ), @@ -1572,7 +1589,7 @@ async def collection_tileset( { "href": self.url_for( request, - "viewer_endpoint", + "map_viewer", tileMatrixSetId=tileMatrixSetId, collectionId=collection.id, ), @@ -1906,12 +1923,12 @@ async def collection_stylejson( if self.with_viewer: @self.router.get( - "/collections/{collectionId}/tiles/{tileMatrixSetId}/viewer", + "/collections/{collectionId}/tiles/{tileMatrixSetId}/map.html", response_class=HTMLResponse, operation_id=".collection.vector.map", tags=["Map Viewer"], ) - def viewer_endpoint( + def map_viewer( request: Request, collection: Annotated[Collection, Depends(self.collection_dependency)], tileMatrixSetId: Annotated[ diff --git a/tipg/main.py b/tipg/main.py index 9915569..50b3ccf 100644 --- a/tipg/main.py +++ b/tipg/main.py @@ -9,6 +9,7 @@ from tipg.collections import register_collection_catalog from tipg.database import close_db_connection, connect_to_db from tipg.errors import DEFAULT_STATUS_CODES, add_exception_handlers +from tipg.extensions.viewer import viewerExtension from tipg.factory import Endpoints from tipg.middleware import CacheControlMiddleware, CatalogUpdateMiddleware from tipg.openapi import _update_openapi @@ -73,6 +74,7 @@ async def lifespan(app: FastAPI): title=settings.name, templates=templates, with_tiles_viewer=settings.add_tiles_viewer, + extensions=[viewerExtension()], ) app.include_router(ogc_api.router) diff --git a/tipg/templates/viewer.html b/tipg/templates/viewer.html new file mode 100644 index 0000000..a78d4d1 --- /dev/null +++ b/tipg/templates/viewer.html @@ -0,0 +1,173 @@ + + +
+