Section2 "Сети" map now shows the OSM engineering-networks layer
(osm_utility_infrastructure_ekb, endpoint /parcels/{cad}/utility-infrastructure)
alongside the NSPD connection-points + ЗОУИТ layers from #1751, on the same map.
- useUtilityInfrastructure hook (react-query, mirrors useConnectionPoints)
- UtilityInfrastructureLayer: Point→CircleMarker, LineString/Polygon(+Multi)→GeoJSON,
per-kind colour (power/water/gas/heat/communication/sewage), popups
- UtilityLayerControlPanel: collapsible legend, per-kind toggle + count
- SiteMap: optional utilityInfrastructure prop + visibleKinds state (CP/ЗОУИТ unchanged)
- 12 unit tests (geometry parsing + [lon,lat]→[lat,lon] swap + grouping)
Refs #1746
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* UtilityLayerControlPanel — #1746: тумблеры слоя инженерной инфраструктуры OSM
|
||
* (по образцу CpLayerControlPanel / ZouitLayerControlPanel). Collapsible,
|
||
* checkbox per-kind с цветным dot + счётчиком рисуемых объектов, «показать все».
|
||
*
|
||
* Счётчики берём из grouped (groupUtilityByKind) — это РИСУЕМЫЕ объекты (валидная
|
||
* геометрия), а не все features: фича без геометрии на карте не появится, поэтому
|
||
* панель не должна обещать её пользователю.
|
||
*/
|
||
|
||
import { useState } from "react";
|
||
|
||
import {
|
||
UTILITY_ALL_KINDS,
|
||
UTILITY_KIND_STYLES,
|
||
type UtilityKind,
|
||
type UtilityMappable,
|
||
} from "@/components/site-finder/UtilityInfrastructureLayer";
|
||
|
||
interface Props {
|
||
grouped: Map<UtilityKind, UtilityMappable[]>;
|
||
visibleKinds: Set<UtilityKind>;
|
||
onToggleKind: (kind: UtilityKind) => void;
|
||
onToggleAll: () => void;
|
||
}
|
||
|
||
export function UtilityLayerControlPanel({
|
||
grouped,
|
||
visibleKinds,
|
||
onToggleKind,
|
||
onToggleAll,
|
||
}: Props) {
|
||
const [collapsed, setCollapsed] = useState(false);
|
||
|
||
const totalCount = UTILITY_ALL_KINDS.reduce(
|
||
(sum, kind) => sum + (grouped.get(kind)?.length ?? 0),
|
||
0,
|
||
);
|
||
|
||
// «Показать все» отражает состояние только непустых (рисуемых) видов.
|
||
const togglableKinds = UTILITY_ALL_KINDS.filter(
|
||
(kind) => (grouped.get(kind)?.length ?? 0) > 0,
|
||
);
|
||
const allVisible =
|
||
togglableKinds.length > 0 &&
|
||
togglableKinds.every((kind) => visibleKinds.has(kind));
|
||
|
||
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" }}>
|
||
Инженерные сети (OSM)
|
||
</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 }}>
|
||
Данные OSM по инж. сетям не получены
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* Toggle-all */}
|
||
<label
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
marginBottom: 6,
|
||
cursor: "pointer",
|
||
fontWeight: 600,
|
||
color: "#374151",
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={allVisible}
|
||
onChange={onToggleAll}
|
||
style={{ cursor: "pointer" }}
|
||
/>
|
||
Показать все ({totalCount})
|
||
</label>
|
||
|
||
{/* Per-kind */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
flexWrap: "wrap",
|
||
gap: "4px 14px",
|
||
}}
|
||
>
|
||
{UTILITY_ALL_KINDS.map((kind) => {
|
||
const items = grouped.get(kind) ?? [];
|
||
if (items.length === 0) return null;
|
||
const style = UTILITY_KIND_STYLES[kind];
|
||
const active = visibleKinds.has(kind);
|
||
return (
|
||
<label
|
||
key={kind}
|
||
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={() => onToggleKind(kind)}
|
||
style={{ cursor: "pointer" }}
|
||
/>
|
||
<span
|
||
style={{
|
||
display: "inline-block",
|
||
width: 10,
|
||
height: 10,
|
||
borderRadius: "50%",
|
||
background: style.color,
|
||
flexShrink: 0,
|
||
}}
|
||
/>
|
||
{style.label}
|
||
<span style={{ color: "#9ca3af" }}>{items.length}</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|