From 6ce634a9b900307e97265b5973d7d1c284a312e5 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Mon, 11 May 2026 21:38:20 +0300 Subject: [PATCH] =?UTF-8?q?feat(site-finder):=20utilities=20(power/pipelin?= =?UTF-8?q?e)=20+=20fix=20=D0=92=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D1=8B=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/parcels.py | 55 +++++++++++++++++++ .../app/services/site_finder/noise_loader.py | 5 ++ .../src/components/site-finder/ScoreCard.tsx | 20 ++++--- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index 5175b8d3..8c8898ab 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -628,6 +628,60 @@ def analyze_parcel( logger.warning("hydrology query failed for %s: %s", cad_num, e) hydrology = None + # 9d) Utilities — power lines + pipelines из OSM (магистральные сети) + utilities: dict[str, Any] | None = None + try: + util_rows = ( + db.execute( + text(""" + SELECT road_class, name, + ST_Distance( + n.geom::geography, + ST_Centroid(ST_GeomFromText(:wkt, 4326))::geography + ) AS distance_m + FROM osm_noise_sources_ekb n + WHERE source_type = 'utility' + AND ST_DWithin( + n.geom::geography, + ST_Centroid(ST_GeomFromText(:wkt, 4326))::geography, + 2000 + ) + ORDER BY distance_m ASC + LIMIT 10 + """), + {"wkt": geom_wkt}, + ) + .mappings() + .all() + ) + # Группировка по типу для compactness + by_subtype: dict[str, dict[str, Any]] = {} + for r in util_rows: + sub = r["road_class"] or "other" + if sub not in by_subtype: + by_subtype[sub] = { + "subtype": sub, + "nearest_m": round(float(r["distance_m"])), + "name": r["name"], + "count_within_2km": 0, + } + by_subtype[sub]["count_within_2km"] += 1 + utilities = { + "summary": list(by_subtype.values()), + "power_line_охранная_зона_flag": any( + float(r["distance_m"]) < 25 and r["road_class"] == "power_line" for r in util_rows + ), + "note": ( + "Охранная зона ЛЭП ≥35 кВ — 15-40м по обе стороны (СП 36.13330). " + "В зоне ОЗ нельзя строить капитальные объекты. " + "Точная классификация напряжения / магистральности — ЗОУИТ " + "5 (ОЗ ЛЭП) и 9 (ОЗ газопровода) через ФГИС ТП." + ), + } + except Exception as e: + logger.warning("utilities query failed for %s: %s", cad_num, e) + utilities = None + # 10) Market trend — динамика цен ДДУ в радиусе 3 км за 6 vs предыдущие 6 месяцев market_trend: dict[str, Any] | None = None try: @@ -748,6 +802,7 @@ def analyze_parcel( "wind": (weather or {}).get("wind") if weather else None, # backward compat "geology": geology, "hydrology": hydrology, + "utilities": utilities, "geotech_risk": _geotech_risk(66, db, geom_wkt), "market_trend": market_trend, } diff --git a/backend/app/services/site_finder/noise_loader.py b/backend/app/services/site_finder/noise_loader.py index d96eb028..bf0084b7 100644 --- a/backend/app/services/site_finder/noise_loader.py +++ b/backend/app/services/site_finder/noise_loader.py @@ -40,6 +40,11 @@ _NOISE_QUERIES: list[tuple[str, str, str, str | None]] = [ ("waterway", "stream", "water", "stream"), ("waterway", "canal", "water", "canal"), ("natural", "water", "water", "lake_or_pond"), + # Инженерные сети — магистральные линии (для proximity-проверки доступности + # подключения / распознавания ЛЭП-охранных зон через ЗОУИТ типа 5/9) + ("power", "line", "utility", "power_line"), + ("power", "minor_line", "utility", "power_line"), + ("man_made", "pipeline", "utility", "pipeline"), ] diff --git a/frontend/src/components/site-finder/ScoreCard.tsx b/frontend/src/components/site-finder/ScoreCard.tsx index 111b41bf..b4b0ef84 100644 --- a/frontend/src/components/site-finder/ScoreCard.tsx +++ b/frontend/src/components/site-finder/ScoreCard.tsx @@ -460,12 +460,18 @@ function WeatherBlock({ - {/* Source + note */} -
- {weather.source} ·{" "} - {weather.note.length > 60 - ? weather.note.slice(0, 60) + "…" - : weather.note} + {/* Source — note shown only on hover */} +
+ {weather.source} · 7 дн.
); @@ -667,7 +673,7 @@ export function ScoreCard({ data }: Props) {