Add project-bound OAuth MCP queries and controlled analysis with scoped evidence, limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs. Default nearby searches to 200 meters and five results. Integrate investigation messaging/presentation, health/readiness and typed history; remove duplicate web search and fix online simulation and compressed baseline writes. Include project migrations 023-025; keep local credentials and demo seeding excluded. Validation: 319 focused pytest checks; real PostGIS temporary-table regression; read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
72 lines
2.7 KiB
Python
72 lines
2.7 KiB
Python
"""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))
|