ZouitLayer (GeoJSON-полигоны из geom_geojson, severity blocker/warning/info по keyword, popup тип+имя) + ZouitLayerControlPanel (collapsible toggle per-severity, счётчик mappable) + SiteMap integration (default blocker+warning ON, info OFF, фон под POI). Graceful: нет geom_geojson → слой/панель скрыты, текстовый NspdZouitOverlapsBlock остаётся. 10 vitest. Closes #255
130 lines
4 KiB
TypeScript
130 lines
4 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* ZouitLayerControlPanel — #255: тумблеры ЗОУИТ-слоя (по образцу
|
||
* CpLayerControlPanel). Collapsible, checkbox per-severity с цветным dot +
|
||
* счётчиком рисуемых полигонов.
|
||
*
|
||
* Счётчики берём из grouped (groupZouitBySeverity) — это РИСУЕМЫЕ объекты
|
||
* (валидная полигональная геометрия), а не все overlaps: ЗОУИТ без geom_geojson
|
||
* на карте не появится, поэтому панель не должна обещать его пользователю.
|
||
*/
|
||
|
||
import { useState } from "react";
|
||
|
||
import {
|
||
ZOUIT_ALL_SEVERITIES,
|
||
ZOUIT_SEVERITY_STYLES,
|
||
type ZouitMappable,
|
||
type ZouitSeverity,
|
||
} from "@/components/site-finder/ZouitLayer";
|
||
|
||
interface Props {
|
||
grouped: Map<ZouitSeverity, ZouitMappable[]>;
|
||
visibleSeverities: Set<ZouitSeverity>;
|
||
onToggleSeverity: (sev: ZouitSeverity) => void;
|
||
}
|
||
|
||
export function ZouitLayerControlPanel({
|
||
grouped,
|
||
visibleSeverities,
|
||
onToggleSeverity,
|
||
}: Props) {
|
||
const [collapsed, setCollapsed] = useState(false);
|
||
|
||
const totalCount = ZOUIT_ALL_SEVERITIES.reduce(
|
||
(sum, sev) => sum + (grouped.get(sev)?.length ?? 0),
|
||
0,
|
||
);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: "1px solid #e5e7eb",
|
||
borderRadius: 10,
|
||
marginTop: 10,
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
{/* Header */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
padding: "8px 12px",
|
||
cursor: "pointer",
|
||
userSelect: "none",
|
||
borderBottom: collapsed ? "none" : "1px solid #f3f4f6",
|
||
}}
|
||
onClick={() => setCollapsed((v) => !v)}
|
||
>
|
||
<span style={{ fontWeight: 700, color: "#1f2937" }}>
|
||
Охранные зоны ЗОУИТ
|
||
</span>
|
||
<span style={{ color: "#6b7280", fontSize: 11 }}>
|
||
{totalCount} зон {collapsed ? "▲" : "▼"}
|
||
</span>
|
||
</div>
|
||
|
||
{!collapsed && (
|
||
<div style={{ padding: "8px 12px 10px" }}>
|
||
{totalCount === 0 ? (
|
||
<div style={{ color: "#6b7280", fontSize: 11 }}>
|
||
Нет геометрии охранных зон для отображения
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
flexWrap: "wrap",
|
||
gap: "4px 14px",
|
||
}}
|
||
>
|
||
{ZOUIT_ALL_SEVERITIES.map((sev) => {
|
||
const items = grouped.get(sev) ?? [];
|
||
if (items.length === 0) return null;
|
||
const style = ZOUIT_SEVERITY_STYLES[sev];
|
||
const active = visibleSeverities.has(sev);
|
||
return (
|
||
<label
|
||
key={sev}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
cursor: "pointer",
|
||
color: "#374151",
|
||
opacity: active ? 1 : 0.45,
|
||
transition: "opacity 0.15s",
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={active}
|
||
onChange={() => onToggleSeverity(sev)}
|
||
style={{ cursor: "pointer" }}
|
||
/>
|
||
<span
|
||
style={{
|
||
display: "inline-block",
|
||
width: 10,
|
||
height: 10,
|
||
borderRadius: 2,
|
||
background: style.color,
|
||
flexShrink: 0,
|
||
}}
|
||
/>
|
||
{style.label}
|
||
<span style={{ color: "#9ca3af" }}>{items.length}</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|