"""РИАСУРТ Свердл 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", ]