— V2MapLegend overlay над cockpit-картой (электр/вода/канал/тепло/газ) с nearest_m + статус-точка, источник utilities.summary (subtype-buckets зеркалят backend _nearest() aliases; electric расширен power_line, gas включает legacy generic pipeline). — Per-theme заголовок карты: dark «СПУТНИК · СВЕЖИЙ СНИМОК», light «СПУТНИК · СХЕМА ПОДКЛЮЧЕНИЙ» (toggle через CSS [data-theme], без JS). — Точечный light-theme contrast в v2.module.css: kv/eng значения, drawer eyebrow, .more / .ver / активная вкладка с cyan→accent-blue, status-dot halo off на белом. Dark не тронут. Scope: только ptica-v2/* и v2.module.css. PticaMapInner (шарится со старым ptica) не тронут.
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* V2MapLegend — compact resource legend overlay rendered ON TOP of the cockpit
|
||
* map card (prototype `ptica-v2/index.html`, block `.legend > .lg-block.lg-res`).
|
||
*
|
||
* Five rows — ЭЛЕКТРИЧЕСТВО / ВОДА / КАНАЛИЗАЦИЯ / ТЕПЛОСНАБЖЕНИЕ / ГАЗ — each
|
||
* showing the distance to the nearest engineering node + a status-dot (green when
|
||
* the узел is within ~1 km, orange when further). Data comes from
|
||
* `analysis.utilities.summary[]` whose `subtype` values are the OSM `road_class`
|
||
* tags produced by the backend (see `noise_loader._NOISE_QUERIES` + `parcels.py`
|
||
* `_nearest()` aliases) — we aggregate them into the 5 prototype buckets.
|
||
*
|
||
* The legend is a SIBLING of `PticaMapInner` inside `V2Map` (the inner Leaflet
|
||
* mount is shared with the старый ptica route and must NOT be touched). All
|
||
* styles live in the scoped v2 module so the overlay never leaks into the
|
||
* application chrome.
|
||
*/
|
||
|
||
import styles from "@/app/site-finder/analysis/[cad]/v2/v2.module.css";
|
||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||
|
||
type ResourceKey = "electric" | "water" | "sewer" | "heat" | "gas";
|
||
|
||
interface ResourceDef {
|
||
key: ResourceKey;
|
||
label: string;
|
||
/** OSM road_class values that count toward this bucket. */
|
||
subtypes: readonly string[];
|
||
/** CSS var for the dot accent color (matches prototype --res-* tokens). */
|
||
color: string;
|
||
}
|
||
|
||
// Aliases mirror backend `_nearest()` in `backend/app/api/v1/parcels.py`:
|
||
// electric ← substation | transformer | power_line
|
||
// water ← water_main | water_works | water_tower
|
||
// sewer ← sewerage
|
||
// heat ← heat_substation
|
||
// gas ← gas_pipeline (legacy generic «pipeline» falls under gas)
|
||
const RESOURCES: readonly ResourceDef[] = [
|
||
{
|
||
key: "electric",
|
||
label: "ЭЛЕКТРИЧЕСТВО",
|
||
subtypes: ["substation", "transformer", "power_line"],
|
||
color: "var(--res-electric)",
|
||
},
|
||
{
|
||
key: "water",
|
||
label: "ВОДА",
|
||
subtypes: ["water_main", "water_works", "water_tower"],
|
||
color: "var(--res-water)",
|
||
},
|
||
{
|
||
key: "sewer",
|
||
label: "КАНАЛИЗАЦИЯ",
|
||
subtypes: ["sewerage"],
|
||
color: "var(--res-sewer)",
|
||
},
|
||
{
|
||
key: "heat",
|
||
label: "ТЕПЛОСНАБЖЕНИЕ",
|
||
subtypes: ["heat_substation"],
|
||
color: "var(--res-heat)",
|
||
},
|
||
{
|
||
key: "gas",
|
||
label: "ГАЗ",
|
||
subtypes: ["gas_pipeline", "pipeline"],
|
||
color: "var(--res-gas)",
|
||
},
|
||
] as const;
|
||
|
||
const OK_THRESHOLD_M = 1000;
|
||
|
||
function formatDistanceMeters(m: number): string {
|
||
// Prototype uses tabular-num, single-line «1 240 м». Round to whole meters.
|
||
return `${Math.round(m).toLocaleString("ru-RU")} м`;
|
||
}
|
||
|
||
function nearestForResource(
|
||
summary: ReadonlyArray<{ subtype: string; nearest_m: number }>,
|
||
subtypes: readonly string[],
|
||
): number | null {
|
||
let best: number | null = null;
|
||
for (const u of summary) {
|
||
if (!subtypes.includes(u.subtype)) continue;
|
||
if (best === null || u.nearest_m < best) best = u.nearest_m;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
interface Props {
|
||
analysis: ParcelAnalysis;
|
||
}
|
||
|
||
export function V2MapLegend({ analysis }: Props) {
|
||
const summary = analysis.utilities?.summary ?? [];
|
||
|
||
return (
|
||
<div className={styles.mapLegend} aria-label="Подключение ресурсов">
|
||
<div className={styles.mapLegendTitle}>ТОЧКИ ПОДКЛЮЧЕНИЯ РЕСУРСОВ</div>
|
||
{RESOURCES.map((res) => {
|
||
const nearest = nearestForResource(summary, res.subtypes);
|
||
const hasData = nearest !== null;
|
||
const ok = hasData && nearest! <= OK_THRESHOLD_M;
|
||
const dotColor = hasData
|
||
? ok
|
||
? "var(--accent-green)"
|
||
: "var(--accent-orange)"
|
||
: "var(--text-soft)";
|
||
return (
|
||
<div
|
||
key={res.key}
|
||
className={styles.mapLegendRow}
|
||
style={{ color: res.color }}
|
||
>
|
||
<span
|
||
className={styles.mapLegendDot}
|
||
style={{ background: dotColor, color: dotColor }}
|
||
aria-hidden="true"
|
||
/>
|
||
<span className={styles.mapLegendLabel}>{res.label}</span>
|
||
<span
|
||
className={`${styles.mapLegendDist} ${hasData ? "" : styles.mapLegendDistEmpty}`}
|
||
>
|
||
{hasData ? formatDistanceMeters(nearest!) : "—"}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|