169 lines
4.6 KiB
Python
169 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.geo import PathPoint
|
|
from app.models.buildings import BuildingsQueryRequest, BuildingsQueryResponse
|
|
|
|
|
|
def _feature_from_value(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, str):
|
|
return json.loads(value)
|
|
return dict(value)
|
|
|
|
|
|
def _geometry_from_geojson(geojson: dict[str, Any]) -> dict[str, Any]:
|
|
if geojson.get("type") == "Feature":
|
|
geometry = geojson.get("geometry")
|
|
else:
|
|
geometry = geojson
|
|
|
|
if not isinstance(geometry, dict) or geometry.get("type") not in {
|
|
"LineString",
|
|
"MultiLineString",
|
|
"Polygon",
|
|
"MultiPolygon",
|
|
}:
|
|
raise ValueError("path must be a GeoJSON geometry or Feature")
|
|
return geometry
|
|
|
|
|
|
def query_buildings(
|
|
request: BuildingsQueryRequest,
|
|
db: Session,
|
|
limit: int = 5000,
|
|
) -> BuildingsQueryResponse:
|
|
try:
|
|
if request.bbox is not None:
|
|
rows = _query_by_bbox(db, request.bbox, limit)
|
|
else:
|
|
rows = _query_by_path(db, request.path or {}, request.buffer_m, limit)
|
|
except SQLAlchemyError as exc:
|
|
raise NotImplementedError(
|
|
"PostGIS buildings table is not ready. Run scripts/load_buildings.sh first."
|
|
) from exc
|
|
|
|
return BuildingsQueryResponse(features=[_feature_from_value(row["feature"]) for row in rows])
|
|
|
|
|
|
def building_heights_along(points: list[PathPoint], db: Session) -> list[float]:
|
|
if not points:
|
|
return []
|
|
|
|
points_json = json.dumps(
|
|
[{"i": index, "lon": point.lon, "lat": point.lat} for index, point in enumerate(points)]
|
|
)
|
|
statement = text(
|
|
"""
|
|
WITH points AS (
|
|
SELECT *
|
|
FROM jsonb_to_recordset(CAST(:points_json AS jsonb))
|
|
AS p(i integer, lon double precision, lat double precision)
|
|
)
|
|
SELECT p.i, COALESCE(MAX(b.height_m), 0) AS height_m
|
|
FROM points p
|
|
LEFT JOIN buildings b
|
|
ON ST_Intersects(b.geom, ST_SetSRID(ST_MakePoint(p.lon, p.lat), 4326))
|
|
GROUP BY p.i
|
|
ORDER BY p.i
|
|
"""
|
|
)
|
|
rows = db.execute(statement, {"points_json": points_json}).mappings().all()
|
|
heights = [0.0] * len(points)
|
|
for row in rows:
|
|
heights[int(row["i"])] = float(row["height_m"] or 0.0)
|
|
return heights
|
|
|
|
|
|
def _query_by_bbox(
|
|
db: Session,
|
|
bbox: tuple[float, float, float, float],
|
|
limit: int,
|
|
) -> list[dict[str, Any]]:
|
|
minlon, minlat, maxlon, maxlat = bbox
|
|
statement = text(
|
|
"""
|
|
SELECT jsonb_build_object(
|
|
'type', 'Feature',
|
|
'id', osm_id,
|
|
'geometry', ST_AsGeoJSON(geom)::jsonb,
|
|
'properties', jsonb_build_object(
|
|
'height_m', height_m,
|
|
'levels', levels,
|
|
'building_type', building_type,
|
|
'source', source
|
|
)
|
|
) AS feature
|
|
FROM buildings
|
|
WHERE geom && ST_MakeEnvelope(:minlon, :minlat, :maxlon, :maxlat, 4326)
|
|
AND ST_Intersects(
|
|
geom,
|
|
ST_MakeEnvelope(:minlon, :minlat, :maxlon, :maxlat, 4326)
|
|
)
|
|
LIMIT :limit
|
|
"""
|
|
)
|
|
return list(
|
|
db.execute(
|
|
statement,
|
|
{
|
|
"minlon": minlon,
|
|
"minlat": minlat,
|
|
"maxlon": maxlon,
|
|
"maxlat": maxlat,
|
|
"limit": limit,
|
|
},
|
|
)
|
|
.mappings()
|
|
.all()
|
|
)
|
|
|
|
|
|
def _query_by_path(
|
|
db: Session,
|
|
path_geojson: dict[str, Any],
|
|
buffer_m: float,
|
|
limit: int,
|
|
) -> list[dict[str, Any]]:
|
|
geometry_json = json.dumps(_geometry_from_geojson(path_geojson))
|
|
statement = text(
|
|
"""
|
|
WITH query_geom AS (
|
|
SELECT ST_Transform(
|
|
ST_Buffer(
|
|
ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:geometry_json), 4326), 3857),
|
|
:buffer_m
|
|
),
|
|
4326
|
|
) AS geom
|
|
)
|
|
SELECT jsonb_build_object(
|
|
'type', 'Feature',
|
|
'id', b.osm_id,
|
|
'geometry', ST_AsGeoJSON(b.geom)::jsonb,
|
|
'properties', jsonb_build_object(
|
|
'height_m', b.height_m,
|
|
'levels', b.levels,
|
|
'building_type', b.building_type,
|
|
'source', b.source
|
|
)
|
|
) AS feature
|
|
FROM buildings b, query_geom q
|
|
WHERE ST_Intersects(b.geom, q.geom)
|
|
LIMIT :limit
|
|
"""
|
|
)
|
|
return list(
|
|
db.execute(
|
|
statement,
|
|
{"geometry_json": geometry_json, "buffer_m": buffer_m, "limit": limit},
|
|
)
|
|
.mappings()
|
|
.all()
|
|
)
|