76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
|
|
import L from "leaflet";
|
|
import type { NodeRecord, CallRecord } from "@/lib/types";
|
|
// Fix Leaflet default icon paths broken by webpack
|
|
delete (L.Icon.Default.prototype as unknown as Record<string, unknown>)._getIconUrl;
|
|
L.Icon.Default.mergeOptions({
|
|
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
|
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
|
|
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
|
});
|
|
|
|
const nodeIcon = (status: string) =>
|
|
L.divIcon({
|
|
className: "",
|
|
html: `<div style="
|
|
width:14px;height:14px;border-radius:50%;
|
|
background:${status === "online" || status === "recording" ? "#4ade80" : status === "unconfigured" ? "#818cf8" : "#6b7280"};
|
|
border:2px solid #111827;
|
|
box-shadow:0 0 6px ${status === "recording" ? "#fb923c" : "transparent"};
|
|
"></div>`,
|
|
iconSize: [14, 14],
|
|
iconAnchor: [7, 7],
|
|
});
|
|
|
|
interface Props {
|
|
nodes: NodeRecord[];
|
|
activeCalls: CallRecord[];
|
|
}
|
|
|
|
export default function MapView({ nodes, activeCalls }: Props) {
|
|
const activeByNode = Object.fromEntries(
|
|
activeCalls.map((c) => [c.node_id, c])
|
|
);
|
|
|
|
const center: [number, number] =
|
|
nodes.length > 0 ? [nodes[0].lat, nodes[0].lon] : [39.5, -98.35];
|
|
|
|
return (
|
|
<MapContainer
|
|
center={center}
|
|
zoom={nodes.length > 0 ? 10 : 4}
|
|
className="w-full h-full rounded-lg"
|
|
style={{ background: "#111827" }}
|
|
>
|
|
<TileLayer
|
|
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
|
attribution='© <a href="https://carto.com/">CARTO</a>'
|
|
/>
|
|
{nodes.map((node) => (
|
|
<Marker
|
|
key={node.node_id}
|
|
position={[node.lat, node.lon]}
|
|
icon={nodeIcon(node.status)}
|
|
>
|
|
<Popup className="font-mono">
|
|
<div className="text-gray-900">
|
|
<p className="font-bold">{node.name}</p>
|
|
<p className="text-xs text-gray-500">{node.node_id}</p>
|
|
<p className="text-xs mt-1 capitalize">{node.status}</p>
|
|
{activeByNode[node.node_id] && (
|
|
<p className="text-xs text-orange-600 mt-1">
|
|
● TG {activeByNode[node.node_id].talkgroup_id ?? "—"}{" "}
|
|
{activeByNode[node.node_id].talkgroup_name}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Popup>
|
|
</Marker>
|
|
))}
|
|
</MapContainer>
|
|
);
|
|
}
|