feat(site-finder): utilities (power/pipeline) + fix Внешние факторы layout

This commit is contained in:
lekss361 2026-05-11 21:38:20 +03:00
parent 2855a01ca7
commit 6ce634a9b9
3 changed files with 73 additions and 7 deletions

View file

@ -628,6 +628,60 @@ def analyze_parcel(
logger.warning("hydrology query failed for %s: %s", cad_num, e) logger.warning("hydrology query failed for %s: %s", cad_num, e)
hydrology = None 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 месяцев # 10) Market trend — динамика цен ДДУ в радиусе 3 км за 6 vs предыдущие 6 месяцев
market_trend: dict[str, Any] | None = None market_trend: dict[str, Any] | None = None
try: try:
@ -748,6 +802,7 @@ def analyze_parcel(
"wind": (weather or {}).get("wind") if weather else None, # backward compat "wind": (weather or {}).get("wind") if weather else None, # backward compat
"geology": geology, "geology": geology,
"hydrology": hydrology, "hydrology": hydrology,
"utilities": utilities,
"geotech_risk": _geotech_risk(66, db, geom_wkt), "geotech_risk": _geotech_risk(66, db, geom_wkt),
"market_trend": market_trend, "market_trend": market_trend,
} }

View file

@ -40,6 +40,11 @@ _NOISE_QUERIES: list[tuple[str, str, str, str | None]] = [
("waterway", "stream", "water", "stream"), ("waterway", "stream", "water", "stream"),
("waterway", "canal", "water", "canal"), ("waterway", "canal", "water", "canal"),
("natural", "water", "water", "lake_or_pond"), ("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"),
] ]

View file

@ -460,12 +460,18 @@ function WeatherBlock({
</div> </div>
</div> </div>
{/* Source + note */} {/* Source — note shown only on hover */}
<div style={{ fontSize: 11, color: "#9ca3af" }} title={weather.note}> <div
{weather.source} ·{" "} style={{
{weather.note.length > 60 fontSize: 11,
? weather.note.slice(0, 60) + "…" color: "#9ca3af",
: weather.note} overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
title={weather.note}
>
{weather.source} · 7 дн.
</div> </div>
</div> </div>
); );
@ -667,7 +673,7 @@ export function ScoreCard({ data }: Props) {
<div <div
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: "repeat(3, 1fr)", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
gap: 10, gap: 10,
}} }}
> >