Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
"""РИАСУРТ Свердл gate-lookup для analyze_parcel (#108, multi-city scaling).
|
||
|
||
Читает ``riasurt_sverdl`` (наполняется harvest-task riasurt_sverdl_harvest.py) и отдаёт зоны
|
||
РИАСУРТ Свердл, пересекающие участок: тер.зона / функц.зона / красные линии / СЗЗ / ЗСО /
|
||
затопление / КРТ. Только для участков агломерации ЕКБ (ОКРАИНЫ — Берёзовский, В.Пышма,
|
||
Среднеуральск, Арамиль, Сысерть), НЕ для ЕКБ-сити (тот не покрыт РИАСУРТ Свердл).
|
||
|
||
Gate-форма (``response["gate"]["riasurt"]``) собирается из topic-бакетов:
|
||
tier / func_zone / red_lines / szz / szo / flood_zone / krt + raw-список overlaps.
|
||
|
||
Graceful: нет таблицы (pre-migration) / пустой WKT / БД-ошибка → пустой gate (analyze не падает).
|
||
Зеркалит стиль ``ird_overlay_lookup.py``.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.exc import DataError, OperationalError, ProgrammingError
|
||
from sqlalchemy.orm import Session
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Кадастровый район ЕКБ-сити — 66:41. Агломерация (окраины) — прочие районы 66:xx
|
||
# (Берёзовский 66:35, В.Пышма/Среднеуральск 66:62, Арамиль/Сысерть 66:25, ...).
|
||
_EKB_CITY_PREFIX = "66:41"
|
||
|
||
# Пустой gate-результат — sentinel (НЕ мутировать).
|
||
RIASURT_EMPTY_GATE: dict[str, Any] = {
|
||
"applicable": False,
|
||
"tier": None,
|
||
"func_zone": None,
|
||
"red_lines": False,
|
||
"szz": False,
|
||
"szo": False,
|
||
"flood_zone": False,
|
||
"krt": False,
|
||
"overlaps": [],
|
||
}
|
||
|
||
# Пересечения участка с зонами РИАСУРТ. geom в БД — EPSG:3857; участок передаём WKT в 4326 и
|
||
# трансформируем в 3857 для ST_Intersects по GIST idx_riasurt_sverdl_geom. CAST psycopg v3.
|
||
_RIASURT_OVERLAP_SQL = text(
|
||
"""
|
||
SELECT source_layer_id, layer_topic, mo_name, obshnz, description
|
||
FROM riasurt_sverdl
|
||
WHERE ST_Intersects(
|
||
geom,
|
||
ST_Transform(ST_GeomFromText(CAST(:parcel_wkt AS text), 4326), 3857)
|
||
)
|
||
ORDER BY layer_topic, source_layer_id
|
||
"""
|
||
)
|
||
|
||
|
||
def is_in_aglomeration_but_not_ekb(cad: str | None) -> bool:
|
||
"""True если участок в агломерации ЕКБ, но НЕ в самом ЕКБ-сити (66:41).
|
||
|
||
РИАСУРТ Свердл покрывает только окраины агломерации (Берёзовский, В.Пышма,
|
||
Среднеуральск, Арамиль, Сысерть) — кадастровые районы 66:xx, кроме 66:41.
|
||
ЕКБ-сити (66:41) и участки вне Свердл (не 66:*) → False.
|
||
|
||
Args:
|
||
cad: кадастровый номер участка ('66:35:...'). None/пусто → False.
|
||
|
||
Returns:
|
||
bool.
|
||
"""
|
||
if not cad:
|
||
return False
|
||
cad = cad.strip()
|
||
if not cad.startswith("66:"):
|
||
return False
|
||
return not cad.startswith(_EKB_CITY_PREFIX + ":") and cad != _EKB_CITY_PREFIX
|
||
|
||
|
||
def parcel_riasurt_gate(
|
||
db: Session,
|
||
parcel_wkt: str | None,
|
||
cad: str | None,
|
||
) -> dict[str, Any]:
|
||
"""Gate-блок РИАСУРТ Свердл для участка агломерации ЕКБ.
|
||
|
||
Если участок не в агломерации (или в ЕКБ-сити) → RIASURT_EMPTY_GATE (applicable=False).
|
||
Иначе ST_Intersects по riasurt_sverdl и сборка topic-бакетов.
|
||
|
||
Args:
|
||
db: сессия.
|
||
parcel_wkt: геометрия участка WKT (EPSG:4326). None → пустой gate.
|
||
cad: кадастровый номер (для is_in_aglomeration_but_not_ekb).
|
||
|
||
Returns:
|
||
dict с полями tier / func_zone / red_lines / szz / szo / flood_zone / krt + overlaps.
|
||
Пусто при отсутствии участка / таблицы / пересечений / вне агломерации.
|
||
"""
|
||
if not parcel_wkt or not is_in_aglomeration_but_not_ekb(cad):
|
||
return dict(RIASURT_EMPTY_GATE)
|
||
|
||
try:
|
||
with db.begin_nested():
|
||
rows = db.execute(_RIASURT_OVERLAP_SQL, {"parcel_wkt": parcel_wkt}).mappings().all()
|
||
except (OperationalError, ProgrammingError, DataError) as exc:
|
||
logger.warning("parcel_riasurt_gate: таблица/запрос недоступны, skip: %s", exc)
|
||
return dict(RIASURT_EMPTY_GATE)
|
||
except Exception as exc:
|
||
logger.warning("parcel_riasurt_gate: неожиданная ошибка, skip: %s", exc)
|
||
return dict(RIASURT_EMPTY_GATE)
|
||
|
||
gate: dict[str, Any] = {
|
||
"applicable": True,
|
||
"tier": None, # description зоны topic='territorial_zone'
|
||
"func_zone": None, # description зоны topic='functional_zone'
|
||
"red_lines": False,
|
||
"szz": False,
|
||
"szo": False,
|
||
"flood_zone": False,
|
||
"krt": False,
|
||
"overlaps": [],
|
||
}
|
||
for r in rows:
|
||
topic = r["layer_topic"]
|
||
gate["overlaps"].append(
|
||
{
|
||
"source_layer_id": r["source_layer_id"],
|
||
"layer_topic": topic,
|
||
"mo_name": r["mo_name"],
|
||
"obshnz": r["obshnz"],
|
||
"description": r["description"],
|
||
}
|
||
)
|
||
if topic == "territorial_zone" and gate["tier"] is None:
|
||
gate["tier"] = r["description"] or r["obshnz"]
|
||
elif topic == "functional_zone" and gate["func_zone"] is None:
|
||
gate["func_zone"] = r["description"] or r["obshnz"]
|
||
elif topic == "red_lines":
|
||
gate["red_lines"] = True
|
||
elif topic == "szz":
|
||
gate["szz"] = True
|
||
elif topic == "szo":
|
||
gate["szo"] = True
|
||
elif topic == "flood_zone":
|
||
gate["flood_zone"] = True
|
||
elif topic == "krt":
|
||
gate["krt"] = True
|
||
|
||
return gate
|
||
|
||
|
||
__all__ = [
|
||
"RIASURT_EMPTY_GATE",
|
||
"is_in_aglomeration_but_not_ekb",
|
||
"parcel_riasurt_gate",
|
||
]
|