"""Bounded nearest-model queries on the already authorized project connection.""" from math import asin, cos, degrees, radians, sin from typing import Any from psycopg import AsyncConnection, sql from app.infra.db.postgresql.read_queries import ELEMENT_VIEWS, read_rows _MODEL_RELATIONS = { kind: sql.Identifier("gis", view) for kind, view in ELEMENT_VIEWS.items() } def _search_boxes( longitude: float, latitude: float, radius_m: float, ) -> list[tuple[float, float, float, float]]: # A deliberately smaller-than-WGS84 curvature radius gives a conservative # geographic envelope. It is only a GiST candidate filter; geography below # determines the actual radius and distances. Split at the antimeridian. angle = (radius_m + 10) / 6_300_000 low, high = latitude - degrees(angle), latitude + degrees(angle) if low < -85 or high > 85: # Avoid Mercator's polar singularity; retain exact geography filtering. return [] width = degrees(asin(min(1.0, sin(angle) / cos(radians(latitude))))) left, right = longitude - width, longitude + width if left < -180: return [(-180, low, right, high), (left + 360, low, 180, high)] if right > 180: return [(left, low, 180, high), (-180, low, right - 360, high)] return [(left, low, right, high)] async def nearby_elements( conn: AsyncConnection, element_type: str, *, center: tuple[float, float], radius_m: float, limit: int, offset: int, ) -> list[dict[str, Any]]: """Rank complete model geometries by spheroidal distance, never endpoints.""" relation = _MODEL_RELATIONS[element_type] filters = [] parameters: list[Any] = [*center] for box in _search_boxes(*center, radius_m): filters.append(sql.SQL( "v.geom && ST_Transform(ST_MakeEnvelope(%s,%s,%s,%s,4326),3857)" )) parameters.extend(box) candidate_filter = sql.SQL(" OR ").join(filters) if filters else sql.SQL("TRUE") statement = sql.SQL(""" WITH target AS ( SELECT ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography AS point ), candidates AS MATERIALIZED ( SELECT v.id, to_jsonb(v)-'geom' AS properties, ST_Transform(v.geom,4326)::geography AS shape FROM {} v WHERE v.geom IS NOT NULL AND NOT ST_IsEmpty(v.geom) AND ({}) ) SELECT c.id AS element_id, c.properties, ST_Distance(c.shape,t.point,true) AS distance_m FROM candidates c CROSS JOIN target t WHERE ST_DWithin(c.shape,t.point,%s,true) ORDER BY distance_m,c.id LIMIT %s OFFSET %s """).format(relation, candidate_filter) parameters.extend((radius_m, limit, offset)) return await read_rows(conn, statement, tuple(parameters))