153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect } from "react"
|
|
import { MapContainer, TileLayer, Polyline, CircleMarker, Popup, useMap } from "react-leaflet"
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
|
import { LatLngBounds, LatLng } from "leaflet"
|
|
import "leaflet/dist/leaflet.css"
|
|
|
|
type Detection = {
|
|
id: number
|
|
type: string
|
|
class: string
|
|
confidence: number
|
|
latitude: number
|
|
longitude: number
|
|
frame_number: number
|
|
}
|
|
|
|
type MapModalProps = {
|
|
open: boolean
|
|
onClose: () => void
|
|
detections: Detection[]
|
|
detectionType: "pothole-detection" | "sign-board-detection"
|
|
}
|
|
|
|
// Component to auto-fit map bounds to show all markers
|
|
function FitBounds({ bounds }: { bounds: LatLngBounds }) {
|
|
const map = useMap()
|
|
|
|
useEffect(() => {
|
|
if (!map || !bounds.isValid()) return
|
|
|
|
// Small delay to ensure the container dimensions are fully calculated (prevents _leaflet_pos error)
|
|
const timer = setTimeout(() => {
|
|
map.invalidateSize()
|
|
map.fitBounds(bounds, {
|
|
padding: [50, 50],
|
|
maxZoom: 16,
|
|
animate: true
|
|
})
|
|
}, 200)
|
|
|
|
return () => clearTimeout(timer)
|
|
}, [bounds, map])
|
|
|
|
return null
|
|
}
|
|
|
|
export default function MapModal({ open, onClose, detections, detectionType }: MapModalProps) {
|
|
// Filter detections with valid GPS coordinates
|
|
const validDetections = detections.filter(d => d.latitude && d.longitude)
|
|
|
|
if (validDetections.length === 0) {
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-4xl h-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Detection Map</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
No GPS coordinates available for detections
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// Extract route coordinates (start to end)
|
|
const routeCoordinates: [number, number][] = validDetections.map(d => [d.latitude, d.longitude])
|
|
|
|
// Calculate bounds to fit all markers
|
|
const bounds = new LatLngBounds(
|
|
new LatLng(routeCoordinates[0][0], routeCoordinates[0][1]),
|
|
new LatLng(routeCoordinates[0][0], routeCoordinates[0][1])
|
|
)
|
|
routeCoordinates.forEach(coord => bounds.extend(new LatLng(coord[0], coord[1])))
|
|
|
|
// Center point (middle of route)
|
|
const center: [number, number] = [
|
|
(bounds.getNorth() + bounds.getSouth()) / 2,
|
|
(bounds.getEast() + bounds.getWest()) / 2
|
|
]
|
|
|
|
const isPothole = detectionType === "pothole-detection"
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-6xl h-[80vh] p-0 flex flex-col overflow-hidden border-none shadow-2xl">
|
|
<DialogHeader className="px-6 pt-6 pb-4 bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800 shrink-0">
|
|
<DialogTitle className="text-xl font-bold">
|
|
{isPothole ? "Pothole" : "Signboard"} Detection Map
|
|
</DialogTitle>
|
|
<p className="text-sm text-muted-foreground">
|
|
{validDetections.length} detection{validDetections.length !== 1 ? "s" : ""} with GPS coordinates
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 w-full bg-gray-50 dark:bg-gray-950 relative">
|
|
<MapContainer
|
|
center={center}
|
|
zoom={13}
|
|
className="h-full w-full"
|
|
scrollWheelZoom={true}
|
|
>
|
|
<TileLayer
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
/>
|
|
|
|
{/* Route line */}
|
|
<Polyline
|
|
positions={routeCoordinates}
|
|
color="#3b82f6"
|
|
weight={3}
|
|
opacity={0.7}
|
|
/>
|
|
|
|
{/* Detection markers */}
|
|
{validDetections.map((detection, idx) => (
|
|
<CircleMarker
|
|
key={`${detection.id}-${idx}`}
|
|
center={[detection.latitude, detection.longitude]}
|
|
radius={8}
|
|
fillColor="#10b981"
|
|
color="#065f46"
|
|
weight={2}
|
|
opacity={1}
|
|
fillOpacity={0.8}
|
|
>
|
|
<Popup>
|
|
<div className="text-xs space-y-1">
|
|
<div className="font-semibold">
|
|
{isPothole ? "Pothole" : detection.class.replace(/_/g, " ")} #{detection.id}
|
|
</div>
|
|
<div>Frame: {detection.frame_number}</div>
|
|
<div>Confidence: {(detection.confidence * 100).toFixed(1)}%</div>
|
|
<div className="font-mono text-[10px]">
|
|
{detection.latitude.toFixed(6)}, {detection.longitude.toFixed(6)}
|
|
</div>
|
|
</div>
|
|
</Popup>
|
|
</CircleMarker>
|
|
))}
|
|
|
|
{/* Auto-fit bounds */}
|
|
<FitBounds bounds={bounds} />
|
|
</MapContainer>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|