gendesign/frontend/src/components/site-finder/ptica-v2/V2MapLegend.tsx
Light1YT c3e091c811
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 52s
CI / openapi-codegen-check (pull_request) Successful in 1m48s
feat(site-finder/v2): resource legend overlay + light theme polish
— 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) не тронут.
2026-06-22 11:26:13 +05:00

133 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}