-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tile server migration #262
base: main
Are you sure you want to change the base?
Changes from 4 commits
704f542
96c2295
1172ce7
4648ae2
266174d
c2995ad
36c4570
138914a
f30d91e
d18576b
0bf5c05
dcfee9c
bbdc599
fc85089
aa6c8b6
fb841df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import logging | ||
|
||
from fastapi import APIRouter, HTTPException | ||
from fastapi.responses import StreamingResponse | ||
|
||
import psycopg2 | ||
from io import BytesIO | ||
|
||
from application.settings import get_settings | ||
|
||
router = APIRouter() | ||
logger = logging.getLogger(__name__) | ||
|
||
DATABASE = {"user": "", "password": "", "host": "", "port": "5432", "database": ""} | ||
|
||
DATABASE_CONNECTION = None | ||
|
||
QUERY_PARAMS = { | ||
"table1": "entity t1", | ||
"srid": "4326", | ||
"geomColumn": "t1.geometry", | ||
"attrColumns": "t1.entity, t1.name, t1.reference", | ||
} | ||
|
||
|
||
# ============================================================ | ||
# Helper Funcs | ||
# ============================================================ | ||
def get_db_connection(): | ||
conn_str = get_settings() | ||
|
||
DATABASE["user"] = conn_str.READ_DATABASE_URL.user | ||
DATABASE["password"] = conn_str.READ_DATABASE_URL.password | ||
DATABASE["host"] = conn_str.READ_DATABASE_URL.host | ||
DATABASE["database"] = conn_str.READ_DATABASE_URL.path.split("/")[1] | ||
|
||
|
||
get_db_connection() | ||
|
||
|
||
# Do the tile x/y coordinates make sense at this zoom level? | ||
def tile_is_valid(tile): | ||
if not ("x" in tile and "y" in tile and "zoom" in tile): | ||
return False | ||
|
||
if "format" not in tile or tile["format"] not in ["pbf", "mvt"]: | ||
return False | ||
|
||
size = 2 ** tile["zoom"] | ||
|
||
if tile["x"] >= size or tile["y"] >= size: | ||
return False | ||
|
||
if tile["x"] < 0 or tile["y"] < 0: | ||
return False | ||
|
||
return True | ||
|
||
|
||
def build_db_query(tile): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should all be using sqlalchemy models not raw sql |
||
qry_params = QUERY_PARAMS.copy() | ||
qry_params["dataset"] = tile["dataset"] | ||
qry_params["x"] = tile["x"] | ||
qry_params["y"] = tile["y"] | ||
qry_params["z"] = tile["zoom"] | ||
|
||
query = """ | ||
WITH | ||
webmercator(envelope) AS ( | ||
SELECT ST_TileEnvelope({z}, {x}, {y}) | ||
), | ||
wgs84(envelope) AS ( | ||
SELECT ST_Transform((SELECT envelope FROM webmercator), {srid}) | ||
), | ||
b(bounds) AS ( | ||
SELECT ST_MakeEnvelope(-180, -85.0511287798066, 180, 85.0511287798066, {srid}) | ||
), | ||
geometries(entity, name, reference, wkb_geometry) AS ( | ||
SELECT | ||
{attrColumns}, | ||
CASE WHEN ST_Covers(b.bounds, {geomColumn}) | ||
THEN ST_Transform({geomColumn},{srid}) | ||
ELSE ST_Transform(ST_Intersection(b.bounds, {geomColumn}),{srid}) | ||
END | ||
FROM | ||
{table1} | ||
CROSS JOIN | ||
b | ||
WHERE | ||
{geomColumn} && (SELECT envelope FROM wgs84) | ||
AND | ||
t1.dataset = '{dataset}' | ||
) | ||
SELECT | ||
ST_AsMVT(tile, '{dataset}') as mvt | ||
FROM ( | ||
SELECT | ||
entity, | ||
name, | ||
reference, | ||
ST_AsMVTGeom(wkb_geometry, (SELECT envelope FROM wgs84)) | ||
FROM geometries | ||
) AS tile | ||
""".format( | ||
**qry_params | ||
) | ||
|
||
return query | ||
|
||
|
||
def sql_to_pbf(sql): | ||
global DATABASE_CONNECTION | ||
|
||
# Make and hold connection to database | ||
if not DATABASE_CONNECTION: | ||
try: | ||
DATABASE_CONNECTION = psycopg2.connect(**DATABASE) | ||
except (Exception, psycopg2.Error) as error: | ||
logger.warning(error) | ||
return None | ||
|
||
# Query for MVT | ||
with DATABASE_CONNECTION.cursor() as cur: | ||
cur.execute(sql) | ||
if not cur: | ||
logger.warning(f"sql query failed: {sql}") | ||
return None | ||
|
||
return cur.fetchone()[0] | ||
|
||
return None | ||
|
||
|
||
# ============================================================ | ||
# API Endpoints | ||
# ============================================================ | ||
|
||
|
||
@router.get("/-/tiles/{dataset}/{z}/{x}/{y}.vector.{fmt}") | ||
async def read_tiles_from_postgres(dataset: str, z: int, x: int, y: int, fmt: str): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is moderately useful as lays out the main router function but should be using different code underneath. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a question over the async nature of this, I'm not sure we use it elsewhere in the application. I'm not against it but would need to check it works okay. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure the router needs to be tiles_ it should probably just be tiles |
||
tile = {"dataset": dataset, "zoom": z, "x": x, "y": y, "format": fmt} | ||
|
||
if not tile_is_valid(tile): | ||
raise HTTPException(status_code=400, detail=f"invalid tile path: {tile}") | ||
|
||
sql = build_db_query(tile) | ||
|
||
pbf = sql_to_pbf(sql) | ||
|
||
pbf_buffer = BytesIO() | ||
pbf_buffer.write(pbf) | ||
pbf_buffer.seek(0) | ||
|
||
resp_headers = { | ||
"Access-Control-Allow-Origin": "*", | ||
"Content-Type": "application/vnd.mapbox-vector-tile", | ||
} | ||
|
||
return StreamingResponse( | ||
pbf_buffer, media_type="vnd.mapbox-vector-tile", headers=resp_headers | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all database info should be managed from the db section this is clearly just development code that was user before. the db folder has the relevant stuff for connecting so don't use whatever is below