Files
road-monitoring-ui/src/components/dashboard/dashboard-map-content.tsx

167 lines
5.3 KiB
TypeScript

'use client';
import { useEffect } from 'react';
import {
MapContainer,
TileLayer,
CircleMarker,
Popup,
useMap,
} from 'react-leaflet';
import { LatLngBounds, LatLng } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { Detection } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface DashboardMapContentProps {
detections: Detection[];
}
// Component to auto-fit map bounds to show all markers
function FitBounds({ bounds }: { bounds: LatLngBounds }) {
const map = useMap();
useEffect(() => {
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [50, 50] });
}
}, [bounds, map]);
return null;
}
export default function DashboardMapContent({
detections,
}: DashboardMapContentProps) {
if (detections.length === 0) {
return (
<div className="h-full w-full flex items-center justify-center bg-muted/20">
<p className="text-muted-foreground">No detections to display</p>
</div>
);
}
// Calculate bounds to fit all markers
const firstDetection = detections[0];
const bounds = new LatLngBounds(
new LatLng(firstDetection.latitude!, firstDetection.longitude!),
new LatLng(firstDetection.latitude!, firstDetection.longitude!),
);
detections.forEach((d) => {
if (d.latitude && d.longitude) {
bounds.extend(new LatLng(d.latitude, d.longitude));
}
});
// Center point
const center: [number, number] = [
(bounds.getNorth() + bounds.getSouth()) / 2,
(bounds.getEast() + bounds.getWest()) / 2,
];
// Get marker color based on detection type
const getMarkerColor = (typeId: string) => {
const config = DETECTION_TYPES[typeId.toLowerCase()];
if (config) {
return {
fill: config.color,
stroke: '#ffffff', // White stroke for better visibility on the map
};
}
return { fill: '#64748b', stroke: '#475569' }; // Default Slate
};
// Get display name for detection type
const getTypeName = (typeId: string) => {
const config = DETECTION_TYPES[typeId.toLowerCase()];
return config ? config.label : typeId.replace(/_/g, ' ');
};
return (
<MapContainer
center={center}
zoom={13}
className="h-full w-full"
scrollWheelZoom={true}
zoomAnimation={false}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* Detection markers */}
{detections.map((detection, idx) => {
const colors = getMarkerColor(detection.type);
const typeName = getTypeName(detection.type);
return (
<CircleMarker
key={`${detection.id}-${idx}`}
center={[detection.latitude!, detection.longitude!]}
radius={10}
fillColor={colors.fill}
color={colors.stroke}
weight={2}
opacity={1}
fillOpacity={0.8}
>
<Popup>
<div className="text-sm min-w-[200px] py-1">
<div className="font-bold text-base border-b border-border pb-2 mb-3 leading-none">
{typeName}
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">Class</span>
<span className="font-medium text-right capitalize">
{detection.class.replace(/_/g, ' ')}
</span>
</div>
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">
Confidence
</span>
<span className="font-medium text-right">
{(detection.confidence * 100).toFixed(1)}%
</span>
</div>
</div>
<div className="pt-2 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground mb-2">
Location Details
</div>
<div className="grid grid-cols-2 gap-3 bg-muted/40 p-2.5 rounded-md font-mono text-[11px]">
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">
Latitude
</span>
<span className="font-medium tracking-tighter">
{detection.latitude!.toFixed(6)}
</span>
</div>
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">
Longitude
</span>
<span className="font-medium tracking-tighter">
{detection.longitude!.toFixed(6)}
</span>
</div>
</div>
</div>
</div>
</Popup>
</CircleMarker>
);
})}
{/* Auto-fit bounds */}
<FitBounds bounds={bounds} />
</MapContainer>
);
}