diff --git a/app/layout.tsx b/app/layout.tsx index 82d5b1f..de17178 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata } from "next" import { Geist, Geist_Mono } from "next/font/google" import { Analytics } from "@vercel/analytics/next" import "./globals.css" +import "leaflet/dist/leaflet.css" const _geist = Geist({ subsets: ["latin"] }) const _geistMono = Geist_Mono({ subsets: ["latin"] }) diff --git a/components/map-modal.tsx b/components/map-modal.tsx new file mode 100644 index 0000000..47ae620 --- /dev/null +++ b/components/map-modal.tsx @@ -0,0 +1,142 @@ +"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 (bounds.isValid()) { + map.fitBounds(bounds, { padding: [50, 50] }) + } + }, [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 ( + + + + Detection Map + +
+ No GPS coordinates available for detections +
+
+
+ ) + } + + // 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 ( + + + + + {isPothole ? "Pothole" : "Signboard"} Detection Map + +

+ {validDetections.length} detection{validDetections.length !== 1 ? "s" : ""} with GPS coordinates +

+
+ +
+ + + + {/* Route line */} + + + {/* Detection markers */} + {validDetections.map((detection, idx) => ( + + +
+
+ {isPothole ? "Pothole" : detection.class.replace(/_/g, " ")} #{detection.id} +
+
Frame: {detection.frame_number}
+
Confidence: {(detection.confidence * 100).toFixed(1)}%
+
+ {detection.latitude.toFixed(6)}, {detection.longitude.toFixed(6)} +
+
+
+
+ ))} + + {/* Auto-fit bounds */} + +
+
+
+
+ ) +} diff --git a/components/video-player-section.tsx b/components/video-player-section.tsx index 94487c1..f4a0896 100644 --- a/components/video-player-section.tsx +++ b/components/video-player-section.tsx @@ -4,7 +4,9 @@ import { useEffect, useRef, useState, useCallback } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Badge } from "@/components/ui/badge" -import { Target, AlertTriangle, Film, Activity, Gauge, Monitor, SignpostBig } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Target, AlertTriangle, Film, Activity, Gauge, Monitor, SignpostBig, Map as MapIcon } from "lucide-react" +import MapModal from "@/components/map-modal" const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1" @@ -143,6 +145,7 @@ function DetailedSummarySection({ }) { const [summaryData, setSummaryData] = useState(null) const [loading, setLoading] = useState(false) + const [showMap, setShowMap] = useState(false) useEffect(() => { if (!show || !videoId || !projectId) return @@ -193,10 +196,23 @@ function DetailedSummarySection({ {/* Location-based Summary */} - Detection Locations - - {isPothole ? "Potholes" : "Signboards"} detected across project locations - +
+
+ Detection Locations + + {isPothole ? "Potholes" : "Signboards"} detected across project locations + +
+ +
@@ -265,6 +281,14 @@ function DetailedSummarySection({ + + {/* Map Modal */} + setShowMap(false)} + detections={allDetections.map(({ detection }) => detection)} + detectionType={detectionType} + />
) } diff --git a/package-lock.json b/package-lock.json index a2e396f..f27525d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "date-fns": "4.1.0", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", + "leaflet": "^1.9.4", "lucide-react": "^0.454.0", "next": "16.0.10", "next-themes": "^0.4.6", @@ -51,6 +52,7 @@ "react-day-picker": "9.8.0", "react-dom": "19.2.0", "react-hook-form": "^7.60.0", + "react-leaflet": "^5.0.0", "react-resizable-panels": "^2.1.7", "recharts": "2.15.4", "sonner": "^1.7.4", @@ -61,6 +63,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.9", + "@types/leaflet": "^1.9.21", "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", @@ -2121,6 +2124,17 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2464,6 +2478,23 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "22.19.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", @@ -2983,6 +3014,13 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -3530,6 +3568,20 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", diff --git a/package.json b/package.json index 60cce49..aff27c6 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "date-fns": "4.1.0", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", + "leaflet": "^1.9.4", "lucide-react": "^0.454.0", "next": "16.0.10", "next-themes": "^0.4.6", @@ -52,6 +53,7 @@ "react-day-picker": "9.8.0", "react-dom": "19.2.0", "react-hook-form": "^7.60.0", + "react-leaflet": "^5.0.0", "react-resizable-panels": "^2.1.7", "recharts": "2.15.4", "sonner": "^1.7.4", @@ -62,6 +64,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.9", + "@types/leaflet": "^1.9.21", "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", @@ -70,4 +73,4 @@ "tw-animate-css": "1.3.3", "typescript": "^5" } -} \ No newline at end of file +}