Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
"""ОКН-lookup для analyze_parcel (#1066).
|
||
|
||
Читает ``okn_objects`` (м.141) и отдаёт объекты культурного наследия
|
||
в заданном радиусе от участка — [{source_id, category, distance_m}].
|
||
|
||
ST_DWithin по geography (метрическое расстояние в метрах) через GIST-индекс
|
||
idx_okn_objects_geom. Участок передаётся как WKT в EPSG:4326.
|
||
|
||
Graceful: если таблица ``okn_objects`` ещё не задеплоена (pre-migration)
|
||
или БД-ошибка — возвращает пустой список (analyze не падает).
|
||
|
||
Wiring в analyze — следующий PR (analyze-wiring, follow-up #1066).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.exc import OperationalError, ProgrammingError
|
||
from sqlalchemy.orm import Session
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Пространственный запрос: ST_DWithin по geography даёт расстояние в метрах.
|
||
# CAST psycopg v3 (никогда :param::type — vault Pattern_CAST_AS_Type).
|
||
_OKN_DWITHIN_SQL = text("""
|
||
SELECT
|
||
source_id,
|
||
category,
|
||
ROUND(
|
||
ST_Distance(
|
||
geom::geography,
|
||
ST_GeomFromText(CAST(:parcel_wkt AS text), 4326)::geography
|
||
)
|
||
)::integer AS distance_m
|
||
FROM okn_objects
|
||
WHERE ST_DWithin(
|
||
geom::geography,
|
||
ST_GeomFromText(CAST(:parcel_wkt AS text), 4326)::geography,
|
||
CAST(:radius_m AS double precision)
|
||
)
|
||
ORDER BY distance_m ASC
|
||
""")
|
||
|
||
|
||
def parcel_okn_objects(
|
||
db: Session,
|
||
parcel_wkt: str | None,
|
||
radius_m: float = 100.0,
|
||
) -> list[dict[str, Any]]:
|
||
"""ОКН-объекты в радиусе radius_m от участка.
|
||
|
||
Args:
|
||
db: SQLAlchemy-сессия.
|
||
parcel_wkt: геометрия участка в WKT (EPSG:4326). None → пустой список.
|
||
radius_m: радиус поиска в метрах (по умолчанию 100 м).
|
||
|
||
Returns:
|
||
Список dict [{source_id, category, distance_m}], отсортированный
|
||
по расстоянию ASC. Пустой список при отсутствии участка / таблицы /
|
||
объектов в радиусе (analyze не падает).
|
||
"""
|
||
if not parcel_wkt:
|
||
return []
|
||
try:
|
||
rows = db.execute(
|
||
_OKN_DWITHIN_SQL,
|
||
{"parcel_wkt": parcel_wkt, "radius_m": radius_m},
|
||
).fetchall()
|
||
except (OperationalError, ProgrammingError) as exc:
|
||
logger.warning("parcel_okn_objects: okn_objects недоступна, skip: %s", exc)
|
||
return []
|
||
except Exception as exc:
|
||
logger.warning("parcel_okn_objects: ошибка запроса okn_objects: %s", exc)
|
||
return []
|
||
|
||
result: list[dict[str, Any]] = []
|
||
for row in rows:
|
||
result.append(
|
||
{
|
||
"source_id": row[0],
|
||
"category": row[1],
|
||
"distance_m": row[2],
|
||
}
|
||
)
|
||
return result
|
||
|
||
|
||
__all__ = ["parcel_okn_objects"]
|