├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.py ├── db.py ├── images └── viewer_demo.png ├── main.py ├── requirements.txt ├── routers ├── __pycache__ │ ├── table.cpython-38.pyc │ └── tiles.cpython-38.pyc ├── table.py └── tiles.py ├── templates ├── index.html └── viewer.html └── utilities.py /.gitignore: -------------------------------------------------------------------------------- 1 | /cache/ 2 | __pycache__/ 3 | .vscode -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | 7 | RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt 8 | 9 | COPY . / 10 | 11 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Keller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastVector 2 | 3 | FastVector is a [PostGIS](https://github.com/postgis/postgis) [vector tiles](https://github.com/mapbox/vector-tile-spec) built for serving large geometric tables. FastVector is written in [Python](https://www.python.org/) using the [FastAPI](https://fastapi.tiangolo.com/) web framework. 4 | 5 | FastVector is built with inspriration from [TiMVT](https://github.com/developmentseed/timvt). 6 | 7 | It defers from TiMVT in the fact that it has multi server/database support, cql_filtering, and a fields parameter. 8 | 9 | --- 10 | 11 | **Source Code**: https://github.com/mkeller3/FastVector 12 | 13 | --- 14 | 15 | ## Requirements 16 | 17 | FastVector requires PostGIS >= 2.4.0. 18 | 19 | ## Configuration 20 | 21 | In order for the api to work you will need to edit the `config.py` file with your database connections. 22 | 23 | Example 24 | ```python 25 | DATABASES = { 26 | "data": { 27 | "host": "localhost", # Hostname of the server 28 | "database": "data", # Name of the database 29 | "username": "postgres", # Name of the user, ideally only SELECT rights 30 | "password": "postgres", # Password of the user 31 | "port": 5432, # Port number for PostgreSQL 32 | "cache_age_in_seconds": 6000, # Number of seconds for tile to be cache in clients browser. You can set to zero if you do not want any caching. 33 | "max_features_per_tile": 100000 # Maximum features per tile. This helps with performance for tables with a large number of rows. 34 | } 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### Running Locally 41 | 42 | To run the app locally `uvicorn main:app --reload` 43 | 44 | ### Production 45 | Build Dockerfile into a docker image to deploy to the cloud. 46 | 47 | ## API 48 | 49 | | Method | URL | Description | 50 | | ------ | -------------------------------------------------------------------------------- | ------------------------------------------------------- | 51 | | `GET` | `/api/v1/table/tables.json` | [Tables](#tables) | 52 | | `GET` | `/api/v1/table/{database}/{scheme}/{table}.json` | [Table JSON](#table-json) | 53 | | `GET` | `/api/v1/tiles/{database}/{scheme}/{table}/{z}/{x}/{y}.pbf` | [Tiles](#tiles) | 54 | | `GET` | `/api/v1/tiles/{database}/{scheme}/{table}.json` | [Table TileJSON](#table-tile-json) | 55 | | `DELETE` | `/api/v1/tiles/cache` | [Delete Cache](#cache-delete) | 56 | | `GET` | `/api/v1/tiles/cache_size` | [Cache Size](#cache-size) | 57 | | `GET` | `/viewer/{database}/{scheme}/{table}` | [Viewer](#viewer) | 58 | | `GET` | `/api/v1/health_check` | Server health check: returns `200 OK` | 59 | 60 | ## Using with Mapbox GL JS 61 | 62 | [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) is a JavaScript library for interactive, customizable vector maps on the web. It takes map styles that conform to the 63 | [Mapbox Style Specification](https://www.mapbox.com/mapbox-gl-js/style-spec), applies them to vector tiles that 64 | conform to the [Mapbox Vector Tile Specification](https://github.com/mapbox/vector-tile-spec), and renders them using 65 | WebGL. 66 | 67 | You can add a layer to the map and specify TileJSON endpoint as a vector source URL. You should also specify a `source-layer` property. For [Table JSON](#table-json) it is `{schema_name}.{table_name}` by default. 68 | 69 | ```js 70 | map.addSource('points', { 71 | type: 'vector', 72 | url: `http://localhost:8000/api/v1/tiles/data/public/state_centroids/{z}/{x}/{y}.pbf` 73 | }); 74 | 75 | map.addLayer({ 76 | 'id': 'state_centroids', 77 | 'type': 'circle', 78 | 'source': 'state_centroids', 79 | 'source-layer': 'public.state_centroids', 80 | 'paint': { 81 | 'circle-color': 'red' 82 | } 83 | }); 84 | ``` 85 | 86 | 87 | ## Using with MapLibre 88 | [MapLibre](https://maplibre.org/projects/maplibre-gl-js/) is an Open-source JavaScript library for publishing maps on your websites. 89 | 90 | ```js 91 | map.addSource('state_centroids', { 92 | type: 'vector', 93 | url: `http://localhost:8000/api/v1/tiles/data/public/state_centroids/{z}/{x}/{y}.pbf` 94 | }); 95 | 96 | map.addLayer({ 97 | 'id': 'points', 98 | 'type': 'circle', 99 | 'source': 'state_centroids', 100 | 'source-layer': 'public.state_centroids', 101 | 'paint': { 102 | 'circle-color': 'red' 103 | } 104 | }); 105 | ``` 106 | 107 | ## Using with Leaflet 108 | 109 | [Leaflet](https://github.com/Leaflet/Leaflet) is the leading open-source JavaScript library for mobile-friendly interactive maps. 110 | 111 | You can add vector tiles using [Leaflet.VectorGrid](https://github.com/Leaflet/Leaflet.VectorGrid) plugin. You must initialize a [VectorGrid.Protobuf](https://leaflet.github.io/Leaflet.VectorGrid/vectorgrid-api-docs.html#vectorgrid-protobuf) with a URL template, just like in L.TileLayers. The difference is that you should define the styling for all the features. 112 | 113 | ```js 114 | L.vectorGrid 115 | .protobuf('http://localhost:8000/api/v1/tiles/data/public/state_centroids/{z}/{x}/{y}.pbf', { 116 | vectorTileLayerStyles: { 117 | 'public.state_centroids': { 118 | color: 'red', 119 | fill: true 120 | } 121 | } 122 | }) 123 | .addTo(map); 124 | ``` 125 | 126 | ## Tables 127 | Tables endpoint provides a listing of all the tables available to query as vector tiles. 128 | 129 | 130 | Tables endpoint is available at `/api/v1/table/tables.json` 131 | 132 | ```shell 133 | curl http://localhost:8000/api/v1/table/tables.json 134 | ``` 135 | 136 | Example Response 137 | ```json 138 | [ 139 | { 140 | "name": "states", 141 | "schema": "public", 142 | "type": "table", 143 | "id": "public.states", 144 | "database": "data", 145 | "detailurl": "http://127.0.0.1:8000/api/v1/table/data/public/states.json", 146 | "viewerurl": "http://127.0.0.1:8000/viewer/data/public/states" 147 | }, 148 | {},... 149 | ``` 150 | 151 | ## Table JSON 152 | 153 | Table endpoint is available at `/api/v1/table/{database}/{scheme}/{table}.json` 154 | 155 | For example, `states` table in `public` schema in `data` database will be available at `/api/v1/table/data/public/states.json` 156 | 157 | ```shell 158 | curl http://localhost:8000/api/v1/table/data/public/states.json 159 | ``` 160 | 161 | Example Response 162 | ```json 163 | { 164 | "id": "public.states", 165 | "schema": "public", 166 | "tileurl": "http://127.0.0.1:8000/api/v1/tiles/data/public/states/{z}/{x}/{y}.pbf", 167 | "viewerurl": "http://127.0.0.1:8000/viewer/data/public/states", 168 | "properties": [ 169 | { 170 | "name": "gid", 171 | "type": "integer", 172 | "description": null 173 | }, 174 | { 175 | "name": "geom", 176 | "type": "geometry", 177 | "description": null 178 | }, 179 | { 180 | "name": "state_name", 181 | "type": "character varying", 182 | "description": null 183 | }, 184 | { 185 | "name": "state_fips", 186 | "type": "character varying", 187 | "description": null 188 | }, 189 | { 190 | "name": "state_abbr", 191 | "type": "character varying", 192 | "description": null 193 | }, 194 | { 195 | "name": "population", 196 | "type": "integer", 197 | "description": null 198 | } 199 | ], 200 | "geometrytype": "ST_MultiPolygon", 201 | "type": "table", 202 | "minzoom": 0, 203 | "maxzoom": 22, 204 | "bounds": [ 205 | -178.2175984, 206 | 18.9217863, 207 | -66.9692709999999, 208 | 71.406235408712 209 | ], 210 | "center": [ 211 | -112.96125695842262, 212 | 45.69082939790446 213 | ] 214 | } 215 | ``` 216 | 217 | ## Tiles 218 | 219 | Tiles endpoint is available at `/api/v1/tiles/{database}/{scheme}/{table}/{z}/{x}/{y}.pbf` 220 | 221 | For example, `states` table in `public` schema in `data` database will be available at `/api/v1/table/data/public/states/{z}/{x}/{y}.pbf` 222 | 223 | ### Fields 224 | 225 | If you have a table with a large amount of fields you can limit the amount of fields returned using the fields parameter. 226 | 227 | #### Note 228 | 229 | If you use the fields parameter the tile will not be cached on the server. 230 | 231 | For example, if we only want the `state_fips` field. 232 | 233 | `/api/v1/table/data/public/states/{z}/{x}/{y}.pbf?fields=state_fips` 234 | 235 | ### CQL Filtering 236 | 237 | CQL filtering is enabled via [pygeofilter](https://pygeofilter.readthedocs.io/en/latest/index.html). This allows you to dynamically filter your tiles database size for larger tiles. 238 | 239 | For example, filter the states layer to only show states with a population greater than 1,000,000. 240 | 241 | `/api/v1/table/data/public/states/{z}/{x}/{y}.pbf?cql_filter=population>1000000` 242 | 243 | [Geoserver](https://docs.geoserver.org/stable/en/user/tutorials/cql/cql_tutorial.html) has examples of using cql filters. 244 | 245 | #### Spatial Filters 246 | 247 | | Filters | 248 | | --- | 249 | | Intersects | 250 | | Equals | 251 | | Disjoint | 252 | | Touches | 253 | | Within | 254 | | Overlaps | 255 | | Crosses | 256 | | Contains | 257 | 258 | #### Note 259 | 260 | If you use the cql_filter parameter the tile will not be cached on the server. 261 | 262 | ## Table Tile JSON 263 | 264 | Table [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint is available at `/api/v1/tiles/{database}/{scheme}/{table}.json` 265 | 266 | For example, `states` table in `public` schema in `data` database will be available at `/api/v1/tiles/data/public/states.json` 267 | 268 | ```shell 269 | curl http://localhost:8000/api/v1/tiles/data/public/states.json 270 | ``` 271 | 272 | Example Response 273 | ```json 274 | { 275 | "tilejson": "2.2.0", 276 | "name": "public.states", 277 | "version": "1.0.0", 278 | "scheme": "xyz", 279 | "tiles": [ 280 | "http://127.0.0.1:8000/api/v1/tiles/data/public/states/{z}/{x}/{y}.pbf" 281 | ], 282 | "viewerurl": "http://127.0.0.1:8000/viewer/data/public/states", 283 | "minzoom": 0, 284 | "maxzoom": 22 285 | } 286 | ``` 287 | 288 | ## Cache Delete 289 | The cache delete endpoint allows you to delete any vector tile cache on the server. 290 | 291 | This is a DELETE HTTP method endpoint. 292 | 293 | In your request you have to pass the following. 294 | 295 | ```json 296 | { 297 | "database": "data", 298 | "scheme": "public", 299 | "table": "states" 300 | } 301 | ``` 302 | 303 | ## Cache Size 304 | Cache Size endpoint allows you to determine the size of a vector tile cache for each table. 305 | 306 | ```shell 307 | curl http://localhost:8000/api/v1/api/v1/tiles/cache_size 308 | ``` 309 | 310 | Example Response 311 | ```json 312 | [ 313 | { 314 | "table": "data_public_counties", 315 | "size_in_gigabytes": 0.004711238 316 | }, 317 | { 318 | "table": "data_public_states", 319 | "size_in_gigabytes": 0.000034666 320 | } 321 | ] 322 | ``` 323 | 324 | ## Viewer 325 | The viewer allows to preview a tile dataset in a web map viewer. 326 | 327 | For example, you can view the states table at `/viewer/data/public/states`. It will automatically zoom to the extent of the table. 328 | 329 | 330 | 331 | ![Viewer Image](/images/viewer_demo.png "Viewer Image") 332 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """FastVector App - Configuration File""" 2 | DATABASES = { 3 | "data": { 4 | "host": "localhost", 5 | "database": "data", 6 | "username": "postgres", 7 | "password": "postgres", 8 | "port": 5432, 9 | "cache_age_in_seconds": 6000, 10 | "max_features_per_tile": 100000 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | """FastVector App - Database Setup""" 2 | from fastapi import FastAPI 3 | import asyncpg 4 | 5 | import config 6 | 7 | async def connect_to_db(app: FastAPI) -> None: 8 | """ 9 | Connect to all databases. 10 | """ 11 | app.state.databases = {} 12 | for database in config.DATABASES.items(): 13 | app.state.databases[f'{database[0]}_pool'] = await asyncpg.create_pool( 14 | dsn=f"postgres://{database[1]['username']}:{database[1]['password']}@{database[1]['host']}:{database[1]['port']}/{database[0]}", 15 | min_size=1, 16 | max_size=10, 17 | max_queries=50000, 18 | max_inactive_connection_lifetime=300, 19 | timeout=180 # 3 Minutes 20 | ) 21 | 22 | async def close_db_connection(app: FastAPI) -> None: 23 | """ 24 | Close connection for all databases. 25 | """ 26 | for database in config.DATABASES: 27 | await app.state.databases[f'{database}_pool'].close() 28 | -------------------------------------------------------------------------------- /images/viewer_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeller3/FastVector/2befa5fc2cdb14e16dbd6345fd3604ebf80e4efc/images/viewer_demo.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | """FastVector App""" 3 | from fastapi import FastAPI, Request 4 | from fastapi.responses import HTMLResponse 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from starlette.templating import Jinja2Templates 7 | from prometheus_fastapi_instrumentator import Instrumentator 8 | 9 | from db import close_db_connection, connect_to_db 10 | from routers import tiles, table 11 | 12 | 13 | templates = Jinja2Templates(directory="templates") 14 | 15 | DESCRIPTION = """ 16 | A lightweight python api to serve vector tiles from PostGIS. 17 | """ 18 | 19 | app = FastAPI( 20 | title="FastVector", 21 | description=DESCRIPTION, 22 | version="0.0.1", 23 | contact={ 24 | "name": "Michael Keller", 25 | "email": "michaelkeller03@gmail.com", 26 | }, 27 | license_info={ 28 | "name": "The MIT License (MIT)", 29 | "url": "https://mit-license.org/", 30 | }, 31 | ) 32 | 33 | app.add_middleware( 34 | CORSMiddleware, 35 | allow_origins=["*"], 36 | allow_credentials=True, 37 | allow_methods=["*"], 38 | allow_headers=["*"], 39 | ) 40 | 41 | app.include_router( 42 | tiles.router, 43 | prefix="/api/v1/tiles", 44 | tags=["Tiles"], 45 | ) 46 | 47 | app.include_router( 48 | table.router, 49 | prefix="/api/v1/table", 50 | tags=["Tables"], 51 | ) 52 | 53 | Instrumentator().instrument(app).expose(app) 54 | 55 | 56 | # Register Start/Stop application event handler to setup/stop the database connection 57 | @app.on_event("startup") 58 | async def startup_event(): 59 | """Application startup: register the database connection and create table list.""" 60 | await connect_to_db(app) 61 | 62 | 63 | @app.on_event("shutdown") 64 | async def shutdown_event(): 65 | """Application shutdown: de-register the database connection.""" 66 | await close_db_connection(app) 67 | 68 | @app.get("/api/v1/health_check", tags=["Health"]) 69 | async def health(): 70 | """ 71 | Method used to verify server is healthy. 72 | """ 73 | 74 | return {"status": "UP"} 75 | 76 | @app.get("/viewer/{database}/{scheme}/{table_name}", response_class=HTMLResponse, tags=["Viewer"]) 77 | async def viewer(request: Request, database: str, scheme: str, table_name: str): 78 | """ 79 | Method used to to view a table in a web map. 80 | """ 81 | 82 | return templates.TemplateResponse("viewer.html", { 83 | "request": request, 84 | "database": database, 85 | "scheme": scheme, 86 | "table": table_name 87 | }) 88 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.68.2 2 | asyncpg==0.25.0 3 | uvicorn==0.17.6 4 | pygeofilter==0.1.2 5 | prometheus_fastapi_instrumentator==5.8.2 -------------------------------------------------------------------------------- /routers/__pycache__/table.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeller3/FastVector/2befa5fc2cdb14e16dbd6345fd3604ebf80e4efc/routers/__pycache__/table.cpython-38.pyc -------------------------------------------------------------------------------- /routers/__pycache__/tiles.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeller3/FastVector/2befa5fc2cdb14e16dbd6345fd3604ebf80e4efc/routers/__pycache__/tiles.cpython-38.pyc -------------------------------------------------------------------------------- /routers/table.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, APIRouter 2 | 3 | import utilities 4 | 5 | router = APIRouter() 6 | 7 | 8 | @router.get("/tables.json", tags=["Tables"]) 9 | async def tables(request: Request): 10 | """ 11 | Method used to return a list of tables available to query for vector tiles. 12 | """ 13 | 14 | def get_detail_url(table: object) -> str: 15 | """Return tile url for layer """ 16 | url = str(request.base_url) 17 | url += f"api/v1/table/{table['database']}/{table['schema']}/{table['name']}.json" 18 | return url 19 | def get_viewer_url(table: object) -> str: 20 | """Return tile url for layer """ 21 | url = str(request.base_url) 22 | url += f"viewer/{table['database']}/{table['schema']}/{table['name']}" 23 | return url 24 | db_tables = await utilities.get_tables_metadata(request.app) 25 | for table in db_tables: 26 | table['detailurl'] = get_detail_url(table) 27 | table['viewerurl'] = get_viewer_url(table) 28 | 29 | return db_tables 30 | 31 | @router.get("/{database}/{scheme}/{table}.json", tags=["Tables"]) 32 | async def table_json(database: str, scheme: str, table: str, request: Request): 33 | """ 34 | Method used to return a information for a given table. 35 | """ 36 | 37 | def get_tile_url() -> str: 38 | """Return tile url for layer """ 39 | url = str(request.base_url) 40 | url += f"api/v1/tiles/{database}/{scheme}/{table}" 41 | url += "/{z}/{x}/{y}.pbf" 42 | 43 | return url 44 | 45 | def get_viewer_url() -> str: 46 | """Return viewer url for layer """ 47 | url = str(request.base_url) 48 | url += f"viewer/{database}/{scheme}/{table}" 49 | 50 | return url 51 | 52 | return { 53 | "id": f"{scheme}.{table}", 54 | "schema": scheme, 55 | "tileurl": get_tile_url(), 56 | "viewerurl": get_viewer_url(), 57 | "properties": await utilities.get_table_columns(database, scheme, table, request.app), 58 | "geometrytype": await utilities.get_table_geometry_type(database, scheme, table, request.app), 59 | "type": "table", 60 | "minzoom": 0, 61 | "maxzoom": 22, 62 | "bounds": await utilities.get_table_bounds(database, scheme, table, request.app), 63 | "center": await utilities.get_table_center(database, scheme, table, request.app) 64 | } 65 | -------------------------------------------------------------------------------- /routers/tiles.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import os 3 | import shutil 4 | from starlette.responses import FileResponse 5 | from fastapi import Response, status, Request, APIRouter 6 | 7 | router = APIRouter() 8 | 9 | import utilities 10 | import config 11 | 12 | @router.get("/{database}/{scheme}/{table}/{z}/{x}/{y}.pbf", tags=["Tiles"]) 13 | async def tiles(database: str, scheme: str, table: str, z: int, x: int, 14 | y: int, request: Request,fields: Optional[str] = None, cql_filter: Optional[str] = None): 15 | """ 16 | Method used to return a vector of tiles for a given table. 17 | """ 18 | 19 | db_settings = config.DATABASES[database] 20 | 21 | pbf, tile_cache = await utilities.get_tile( 22 | database, 23 | scheme, 24 | table, 25 | z, 26 | x, 27 | y, 28 | fields, 29 | cql_filter, 30 | db_settings, 31 | request.app 32 | ) 33 | 34 | response_code = status.HTTP_200_OK 35 | 36 | max_cache_age = db_settings['cache_age_in_seconds'] 37 | 38 | if fields is not None and cql_filter is not None: 39 | max_cache_age = 0 40 | 41 | if tile_cache: 42 | return FileResponse( 43 | path=f'{os.getcwd()}/cache/{database}_{scheme}_{table}/{z}/{x}/{y}', 44 | media_type="application/vnd.mapbox-vector-tile", 45 | status_code=response_code, 46 | headers = { 47 | "Cache-Control": f"max-age={max_cache_age}", 48 | "tile-cache": 'true' 49 | } 50 | ) 51 | 52 | if pbf == b"": 53 | response_code = status.HTTP_204_NO_CONTENT 54 | 55 | return Response( 56 | content=bytes(pbf), 57 | media_type="application/vnd.mapbox-vector-tile", 58 | status_code=response_code, 59 | headers = { 60 | "Cache-Control": f"max-age={max_cache_age}", 61 | "tile-cache": 'false' 62 | } 63 | ) 64 | 65 | @router.get("/{database}/{scheme}/{table}.json", tags=["Tiles"]) 66 | async def tiles_json(database: str, scheme: str, table: str, request: Request): 67 | """ 68 | Method used to return a tilejson information for a given table. 69 | """ 70 | 71 | def get_tile_url() -> str: 72 | """Return tile url for layer """ 73 | url = str(request.base_url) 74 | url += f"api/v1/tiles/{database}/{scheme}/{table}" 75 | url += "/{z}/{x}/{y}.pbf" 76 | return url 77 | 78 | def get_viewer_url() -> str: 79 | """Return viewer url for layer """ 80 | url = str(request.base_url) 81 | url += f"viewer/{database}/{scheme}/{table}" 82 | return url 83 | 84 | return { 85 | "tilejson": "2.2.0", 86 | "name": f"{scheme}.{table}", 87 | "version": "1.0.0", 88 | "scheme": "xyz", 89 | "tiles": [ 90 | get_tile_url() 91 | ], 92 | "viewerurl": get_viewer_url(), 93 | "minzoom": 0, 94 | "maxzoom": 22, 95 | } 96 | 97 | @router.get("/cache_size", tags=["Tiles"]) 98 | async def get_tile_cache_size(): 99 | """ 100 | Method used to a list of cache sizes for each table that has cache. 101 | """ 102 | 103 | cache_sizes = [] 104 | 105 | def get_size(start_path = '.'): 106 | total_size = 0 107 | for dirpath, dirnames, filenames in os.walk(start_path): 108 | for file in filenames: 109 | file_path = os.path.join(dirpath, file) 110 | if not os.path.islink(file_path): 111 | total_size += os.path.getsize(file_path) 112 | 113 | return total_size 114 | 115 | cache_folders = os.listdir(f'{os.getcwd()}/cache/') 116 | 117 | for folder in cache_folders: 118 | cache_sizes.append( 119 | { 120 | "table": folder, 121 | "size_in_gigabytes": get_size(f'{os.getcwd()}/cache/{folder}')*.000000001 122 | } 123 | ) 124 | 125 | return cache_sizes 126 | 127 | @router.delete("/cache", tags=["Tiles"]) 128 | async def delete_tile_cache(database: str, scheme: str, table: str): 129 | """ 130 | Method used to delete cache for a table. 131 | """ 132 | 133 | if os.path.exists(f'{os.getcwd()}/cache/{database}_{scheme}_{table}'): 134 | shutil.rmtree(f'{os.getcwd()}/cache/{database}_{scheme}_{table}') 135 | return {"status": "deleted"} 136 | else: 137 | return {"error": f"No cache at {os.getcwd()}/cache/{database}_{scheme}_{table}"} 138 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FastVector 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 22 |
23 |

A lightweight vector tile api built on top of FastAPI.

24 | 25 | 26 | Vector Tile Endpoints 27 | 28 | 29 | {%for table in tables %} 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
{{table.name}}
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /templates/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FastVector - Tile Viewer 7 | 8 | 9 | 10 | 23 | 24 | 25 | 26 |
27 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /utilities.py: -------------------------------------------------------------------------------- 1 | """FastVector App - Utilities""" 2 | import os 3 | import json 4 | from fastapi import FastAPI 5 | from pygeofilter.backends.sql import to_sql_where 6 | from pygeofilter.parsers.ecql import parse 7 | 8 | import config 9 | 10 | async def get_tile(database: str, scheme: str, table: str, z: int, 11 | x: int, y: int, fields: str, cql_filter: str, db_settings: object, app: FastAPI) -> bytes: 12 | """ 13 | Method to return vector tile from database. 14 | """ 15 | 16 | cachefile = f'{os.getcwd()}/cache/{database}_{scheme}_{table}/{z}/{x}/{y}' 17 | 18 | if os.path.exists(cachefile): 19 | return '', True 20 | 21 | pool = app.state.databases[f'{database}_pool'] 22 | 23 | async with pool.acquire() as con: 24 | 25 | 26 | sql_field_query = f""" 27 | SELECT column_name 28 | FROM information_schema.columns 29 | WHERE table_name = '{table}' 30 | AND column_name != 'geom'; 31 | """ 32 | 33 | field_mapping = {} 34 | 35 | db_fields = await con.fetch(sql_field_query) 36 | 37 | for field in db_fields: 38 | field_mapping[field['column_name']] = field['column_name'] 39 | 40 | if fields is None: 41 | field_list = "" 42 | 43 | for field in db_fields: 44 | field_list += f", {field['column_name']}" 45 | else: 46 | field_list = f",{fields}" 47 | 48 | sql_vector_query = f""" 49 | SELECT ST_AsMVT(tile, '{scheme}.{table}', 4096) 50 | FROM ( 51 | WITH 52 | bounds AS ( 53 | SELECT ST_TileEnvelope({z}, {x}, {y}) as geom 54 | ) 55 | SELECT 56 | st_asmvtgeom( 57 | ST_Transform(t.geom, 3857) 58 | ,bounds.geom 59 | ) AS mvtgeom {field_list} 60 | FROM {scheme}.{table} as t, bounds 61 | WHERE ST_Intersects( 62 | ST_Transform(t.geom, 4326), 63 | ST_Transform(bounds.geom, 4326) 64 | ) 65 | 66 | """ 67 | if cql_filter: 68 | ast = parse(cql_filter) 69 | where_statement = to_sql_where(ast, field_mapping) 70 | sql_vector_query += f" AND {where_statement}" 71 | 72 | sql_vector_query += f"LIMIT {db_settings['max_features_per_tile']}) as tile" 73 | 74 | tile = await con.fetchval(sql_vector_query) 75 | 76 | if fields is None and cql_filter is None and db_settings['cache_age_in_seconds'] > 0: 77 | 78 | cachefile_dir = f'{os.getcwd()}/cache/{database}_{scheme}_{table}/{z}/{x}' 79 | 80 | if not os.path.exists(cachefile_dir): 81 | try: 82 | os.makedirs(cachefile_dir) 83 | except OSError: 84 | pass 85 | 86 | with open(cachefile, "wb") as file: 87 | file.write(tile) 88 | file.close() 89 | 90 | return tile, False 91 | 92 | async def get_tables_metadata(app: FastAPI) -> list: 93 | """ 94 | Method used to get tables metadata. 95 | """ 96 | tables_metadata = [] 97 | 98 | for database in config.DATABASES.items(): 99 | 100 | pool = app.state.databases[f'{database[0]}_pool'] 101 | 102 | async with pool.acquire() as con: 103 | tables_query = """ 104 | SELECT schemaname, tablename 105 | FROM pg_catalog.pg_tables 106 | WHERE schemaname not in ('pg_catalog','information_schema', 'topology') 107 | AND tablename != 'spatial_ref_sys'; 108 | """ 109 | tables = await con.fetch(tables_query) 110 | for table in tables: 111 | tables_metadata.append( 112 | { 113 | "name" : table['tablename'], 114 | "schema" : table['schemaname'], 115 | "type" : "table", 116 | "id" : f"{table['schemaname']}.{table['tablename']}", 117 | "database" : config.DATABASES[database[0]]['database'] 118 | } 119 | ) 120 | 121 | return tables_metadata 122 | 123 | async def get_table_columns(database: str, scheme: str, table: str, app: FastAPI) -> list: 124 | """ 125 | Method used to retrieve columns for a given table. 126 | """ 127 | 128 | pool = app.state.databases[f'{database}_pool'] 129 | 130 | async with pool.acquire() as con: 131 | column_query = f""" 132 | SELECT 133 | jsonb_agg( 134 | jsonb_build_object( 135 | 'name', attname, 136 | 'type', format_type(atttypid, null), 137 | 'description', col_description(attrelid, attnum) 138 | ) 139 | ) 140 | FROM pg_attribute 141 | WHERE attnum>0 142 | AND attrelid=format('%I.%I', '{scheme}', '{table}')::regclass 143 | """ 144 | columns = await con.fetchval(column_query) 145 | 146 | return json.loads(columns) 147 | 148 | async def get_table_geometry_type(database: str, scheme: str, table: str, app: FastAPI) -> list: 149 | """ 150 | Method used to retrieve the geometry type for a given table. 151 | """ 152 | 153 | pool = app.state.databases[f'{database}_pool'] 154 | 155 | async with pool.acquire() as con: 156 | geometry_query = f""" 157 | SELECT ST_GeometryType(geom) as geom_type 158 | FROM {scheme}.{table} 159 | """ 160 | geometry_type = await con.fetchval(geometry_query) 161 | 162 | return geometry_type 163 | 164 | async def get_table_center(database: str, scheme: str, table: str, app: FastAPI) -> list: 165 | """ 166 | Method used to retrieve the table center for a given table. 167 | """ 168 | 169 | pool = app.state.databases[f'{database}_pool'] 170 | 171 | async with pool.acquire() as con: 172 | query = f""" 173 | SELECT ST_X(ST_Centroid(ST_Union(geom))) as x, 174 | ST_Y(ST_Centroid(ST_Union(geom))) as y 175 | FROM {scheme}.{table} 176 | """ 177 | center = await con.fetch(query) 178 | 179 | return [center[0][0],center[0][1]] 180 | 181 | async def get_table_bounds(database: str, scheme: str, table: str, app: FastAPI) -> list: 182 | """ 183 | Method used to retrieve the bounds for a given table. 184 | """ 185 | 186 | pool = app.state.databases[f'{database}_pool'] 187 | 188 | async with pool.acquire() as con: 189 | query = f""" 190 | SELECT ARRAY[ 191 | ST_XMin(ST_Union(geom)), 192 | ST_YMin(ST_Union(geom)), 193 | ST_XMax(ST_Union(geom)), 194 | ST_YMax(ST_Union(geom)) 195 | ] 196 | FROM {scheme}.{table} 197 | """ 198 | extent = await con.fetchval(query) 199 | 200 | return extent 201 | --------------------------------------------------------------------------------