added more feature according to updated model
This commit is contained in:
@@ -55,57 +55,102 @@ interface ProjectSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DetectionStats {
|
interface DetectionStats {
|
||||||
totalPotholes: number
|
totalDefectedSignboard: number
|
||||||
totalSignboards: number
|
totalPothole: number
|
||||||
totalDetections: number
|
totalRoadCrack: number
|
||||||
|
totalDamagedRoadMarking: number
|
||||||
|
totalGoodSignboard: number
|
||||||
|
totalRoadDamage: number
|
||||||
locationData: Array<{
|
locationData: Array<{
|
||||||
name: string
|
name: string
|
||||||
potholes: number
|
defected_sign_board: number
|
||||||
signboards: number
|
pothole: number
|
||||||
|
road_crack: number
|
||||||
|
damaged_road_marking: number
|
||||||
|
good_sign_board: number
|
||||||
total: number
|
total: number
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
function calculateStats(summary: ProjectSummary | null): DetectionStats {
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
return { totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] }
|
return {
|
||||||
|
totalDefectedSignboard: 0,
|
||||||
|
totalPothole: 0,
|
||||||
|
totalRoadCrack: 0,
|
||||||
|
totalDamagedRoadMarking: 0,
|
||||||
|
totalGoodSignboard: 0,
|
||||||
|
totalRoadDamage: 0,
|
||||||
|
locationData: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalPotholes = 0
|
let totalDefectedSignboard = 0
|
||||||
let totalSignboards = 0
|
let totalPothole = 0
|
||||||
|
let totalRoadCrack = 0
|
||||||
|
let totalDamagedRoadMarking = 0
|
||||||
|
let totalGoodSignboard = 0
|
||||||
|
let totalRoadDamage = 0
|
||||||
const locationData: DetectionStats["locationData"] = []
|
const locationData: DetectionStats["locationData"] = []
|
||||||
|
|
||||||
for (const pkg of Object.values(summary.packages || {})) {
|
for (const pkg of Object.values(summary.packages || {})) {
|
||||||
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
|
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
|
||||||
let locPotholes = 0
|
let locDefectedSignboard = 0
|
||||||
let locSignboards = 0
|
let locPothole = 0
|
||||||
|
let locRoadCrack = 0
|
||||||
|
let locDamagedRoadMarking = 0
|
||||||
|
let locGoodSignboard = 0
|
||||||
|
|
||||||
for (const detection of loc.detections || []) {
|
for (const detection of loc.detections || []) {
|
||||||
if (detection.type.toLowerCase().includes("pothole")) {
|
const type = detection.type.toLowerCase()
|
||||||
locPotholes++
|
if (type === "defected_sign_board") {
|
||||||
totalPotholes++
|
locDefectedSignboard++
|
||||||
} else {
|
totalDefectedSignboard++
|
||||||
locSignboards++
|
} else if (type === "pothole") {
|
||||||
totalSignboards++
|
locPothole++
|
||||||
|
totalPothole++
|
||||||
|
} else if (type === "road_crack") {
|
||||||
|
locRoadCrack++
|
||||||
|
totalRoadCrack++
|
||||||
|
} else if (type === "damaged_road_marking") {
|
||||||
|
locDamagedRoadMarking++
|
||||||
|
totalDamagedRoadMarking++
|
||||||
|
} else if (type === "good_sign_board") {
|
||||||
|
locGoodSignboard++
|
||||||
|
totalGoodSignboard++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locPotholes > 0 || locSignboards > 0) {
|
const locTotalDamage = locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking
|
||||||
|
totalRoadDamage += (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0) ? 1 : 0 // This logic might need refinement based on "total_road_damage" definition
|
||||||
|
|
||||||
|
if (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0 || locGoodSignboard > 0) {
|
||||||
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
||||||
locationData.push({
|
locationData.push({
|
||||||
name: shortName,
|
name: shortName,
|
||||||
potholes: locPotholes,
|
defected_sign_board: locDefectedSignboard,
|
||||||
signboards: locSignboards,
|
pothole: locPothole,
|
||||||
total: locPotholes + locSignboards
|
road_crack: locRoadCrack,
|
||||||
|
damaged_road_marking: locDamagedRoadMarking,
|
||||||
|
good_sign_board: locGoodSignboard,
|
||||||
|
total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recalculate totalRoadDamage based on backends unique count
|
||||||
|
// But since we are aggregating from locations, we just sum them up or use a simpler metric
|
||||||
|
// The user's JSON shows "total_road_damage": 9 which is sum of 2+6+0+1
|
||||||
|
totalRoadDamage = totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalPotholes,
|
totalDefectedSignboard,
|
||||||
totalSignboards,
|
totalPothole,
|
||||||
totalDetections: totalPotholes + totalSignboards,
|
totalRoadCrack,
|
||||||
|
totalDamagedRoadMarking,
|
||||||
|
totalGoodSignboard,
|
||||||
|
totalRoadDamage,
|
||||||
locationData
|
locationData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,9 +161,12 @@ export default function DashboardPage() {
|
|||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||||
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null)
|
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null)
|
||||||
const [stats, setStats] = useState<DetectionStats>({
|
const [stats, setStats] = useState<DetectionStats>({
|
||||||
totalPotholes: 0,
|
totalDefectedSignboard: 0,
|
||||||
totalSignboards: 0,
|
totalPothole: 0,
|
||||||
totalDetections: 0,
|
totalRoadCrack: 0,
|
||||||
|
totalDamagedRoadMarking: 0,
|
||||||
|
totalGoodSignboard: 0,
|
||||||
|
totalRoadDamage: 0,
|
||||||
locationData: []
|
locationData: []
|
||||||
})
|
})
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -218,12 +266,23 @@ export default function DashboardPage() {
|
|||||||
// Filter stats based on selections
|
// Filter stats based on selections
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectSummary) {
|
if (!projectSummary) {
|
||||||
setStats({ totalPotholes: 0, totalSignboards: 0, totalDetections: 0, locationData: [] })
|
setStats({
|
||||||
|
totalDefectedSignboard: 0,
|
||||||
|
totalPothole: 0,
|
||||||
|
totalRoadCrack: 0,
|
||||||
|
totalDamagedRoadMarking: 0,
|
||||||
|
totalGoodSignboard: 0,
|
||||||
|
totalRoadDamage: 0,
|
||||||
|
locationData: []
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalPotholes = 0
|
let totalDefectedSignboard = 0
|
||||||
let totalSignboards = 0
|
let totalPothole = 0
|
||||||
|
let totalRoadCrack = 0
|
||||||
|
let totalDamagedRoadMarking = 0
|
||||||
|
let totalGoodSignboard = 0
|
||||||
const locationData: DetectionStats["locationData"] = []
|
const locationData: DetectionStats["locationData"] = []
|
||||||
|
|
||||||
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
|
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
|
||||||
@@ -238,35 +297,54 @@ export default function DashboardPage() {
|
|||||||
for (const [locName, loc] of Object.entries(locationsToProcess)) {
|
for (const [locName, loc] of Object.entries(locationsToProcess)) {
|
||||||
if (!loc) continue
|
if (!loc) continue
|
||||||
|
|
||||||
let locPotholes = 0
|
let locDefectedSignboard = 0
|
||||||
let locSignboards = 0
|
let locPothole = 0
|
||||||
|
let locRoadCrack = 0
|
||||||
|
let locDamagedRoadMarking = 0
|
||||||
|
let locGoodSignboard = 0
|
||||||
|
|
||||||
for (const detection of loc.detections || []) {
|
for (const detection of loc.detections || []) {
|
||||||
if (detection.type.toLowerCase().includes("pothole")) {
|
const type = detection.type.toLowerCase()
|
||||||
locPotholes++
|
if (type === "defected_sign_board") {
|
||||||
totalPotholes++
|
locDefectedSignboard++
|
||||||
} else {
|
totalDefectedSignboard++
|
||||||
locSignboards++
|
} else if (type === "pothole") {
|
||||||
totalSignboards++
|
locPothole++
|
||||||
|
totalPothole++
|
||||||
|
} else if (type === "road_crack") {
|
||||||
|
locRoadCrack++
|
||||||
|
totalRoadCrack++
|
||||||
|
} else if (type === "damaged_road_marking") {
|
||||||
|
locDamagedRoadMarking++
|
||||||
|
totalDamagedRoadMarking++
|
||||||
|
} else if (type === "good_sign_board") {
|
||||||
|
locGoodSignboard++
|
||||||
|
totalGoodSignboard++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locPotholes > 0 || locSignboards > 0) {
|
if (locDefectedSignboard > 0 || locPothole > 0 || locRoadCrack > 0 || locDamagedRoadMarking > 0 || locGoodSignboard > 0) {
|
||||||
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
const shortName = locName.length > 20 ? locName.substring(0, 20) + "..." : locName
|
||||||
locationData.push({
|
locationData.push({
|
||||||
name: shortName,
|
name: shortName,
|
||||||
potholes: locPotholes,
|
defected_sign_board: locDefectedSignboard,
|
||||||
signboards: locSignboards,
|
pothole: locPothole,
|
||||||
total: locPotholes + locSignboards
|
road_crack: locRoadCrack,
|
||||||
|
damaged_road_marking: locDamagedRoadMarking,
|
||||||
|
good_sign_board: locGoodSignboard,
|
||||||
|
total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
totalPotholes,
|
totalDefectedSignboard,
|
||||||
totalSignboards,
|
totalPothole,
|
||||||
totalDetections: totalPotholes + totalSignboards,
|
totalRoadCrack,
|
||||||
|
totalDamagedRoadMarking,
|
||||||
|
totalGoodSignboard,
|
||||||
|
totalRoadDamage: totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking,
|
||||||
locationData
|
locationData
|
||||||
})
|
})
|
||||||
}, [projectSummary, selectedPackageId, selectedLocationId])
|
}, [projectSummary, selectedPackageId, selectedLocationId])
|
||||||
@@ -311,29 +389,53 @@ export default function DashboardPage() {
|
|||||||
{/* Stats Cards - Top Row */}
|
{/* Stats Cards - Top Row */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
<GradientStatsCard
|
<GradientStatsCard
|
||||||
title="Total Detections"
|
title="Total Road Damage"
|
||||||
subtitle="All detected objects"
|
subtitle="Combined damage detections"
|
||||||
value={stats.totalDetections}
|
value={stats.totalRoadDamage}
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
gradient="purple"
|
gradient="purple"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
<GradientStatsCard
|
<GradientStatsCard
|
||||||
title="Potholes"
|
title="Potholes"
|
||||||
subtitle="Road surface damage"
|
subtitle="Surface depressions"
|
||||||
value={stats.totalPotholes}
|
value={stats.totalPothole}
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
gradient="green"
|
gradient="green"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
<GradientStatsCard
|
<GradientStatsCard
|
||||||
title="Signboards"
|
title="Defected Signboards"
|
||||||
subtitle="Traffic signs detected"
|
subtitle="Damaged traffic signs"
|
||||||
value={stats.totalSignboards}
|
value={stats.totalDefectedSignboard}
|
||||||
icon={RectangleHorizontal}
|
icon={RectangleHorizontal}
|
||||||
gradient="blue"
|
gradient="blue"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Road Cracks"
|
||||||
|
subtitle="Surface fissures"
|
||||||
|
value={stats.totalRoadCrack}
|
||||||
|
icon={AlertTriangle}
|
||||||
|
gradient="orange"
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Damaged Markings"
|
||||||
|
subtitle="Worn road lines"
|
||||||
|
value={stats.totalDamagedRoadMarking}
|
||||||
|
icon={TrendingUp}
|
||||||
|
gradient="indigo"
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<GradientStatsCard
|
||||||
|
title="Good Signboards"
|
||||||
|
subtitle="Informational markers"
|
||||||
|
value={stats.totalGoodSignboard}
|
||||||
|
icon={RectangleHorizontal}
|
||||||
|
gradient="emerald"
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Selector - Above main content */}
|
{/* Filter Selector - Above main content */}
|
||||||
@@ -368,8 +470,11 @@ export default function DashboardPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<DetectionDonutChart
|
<DetectionDonutChart
|
||||||
potholes={stats.totalPotholes}
|
defectedSignboard={stats.totalDefectedSignboard}
|
||||||
signboards={stats.totalSignboards}
|
pothole={stats.totalPothole}
|
||||||
|
roadCrack={stats.totalRoadCrack}
|
||||||
|
damagedRoadMarking={stats.totalDamagedRoadMarking}
|
||||||
|
goodSignboard={stats.totalGoodSignboard}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -389,7 +494,15 @@ export default function DashboardPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<LocationBarChart
|
<LocationBarChart
|
||||||
data={stats.locationData}
|
data={stats.locationData.map(loc => ({
|
||||||
|
name: loc.name,
|
||||||
|
defected_sign_board: loc.defected_sign_board,
|
||||||
|
pothole: loc.pothole,
|
||||||
|
road_crack: loc.road_crack,
|
||||||
|
damaged_road_marking: loc.damaged_road_marking,
|
||||||
|
good_sign_board: loc.good_sign_board,
|
||||||
|
total: loc.total
|
||||||
|
}))}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -175,6 +175,7 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-scroll-locked] {
|
body[data-scroll-locked] {
|
||||||
|
|||||||
@@ -53,18 +53,24 @@ export default function DashboardMapContent({ detections }: DashboardMapContentP
|
|||||||
|
|
||||||
// Get marker color based on detection type
|
// Get marker color based on detection type
|
||||||
const getMarkerColor = (type: string) => {
|
const getMarkerColor = (type: string) => {
|
||||||
if (type.toLowerCase().includes("pothole")) {
|
const t = type.toLowerCase()
|
||||||
return { fill: "#10b981", stroke: "#065f46" } // Green for potholes
|
if (t === "pothole") {
|
||||||
|
return { fill: "#ef4444", stroke: "#b91c1c" } // Red
|
||||||
|
} else if (t === "defected_sign_board") {
|
||||||
|
return { fill: "#3b82f6", stroke: "#1d4ed8" } // Blue
|
||||||
|
} else if (t === "road_crack") {
|
||||||
|
return { fill: "#f59e0b", stroke: "#b45309" } // Orange
|
||||||
|
} else if (t === "damaged_road_marking") {
|
||||||
|
return { fill: "#6366f1", stroke: "#4338ca" } // Indigo
|
||||||
|
} else if (t === "good_sign_board") {
|
||||||
|
return { fill: "#10b981", stroke: "#047857" } // Emerald
|
||||||
}
|
}
|
||||||
return { fill: "#3b82f6", stroke: "#2563eb" } // Blue for signboards
|
return { fill: "#64748b", stroke: "#475569" } // Default Slate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get display name for detection type
|
// Get display name for detection type
|
||||||
const getTypeName = (type: string) => {
|
const getTypeName = (type: string) => {
|
||||||
if (type.toLowerCase().includes("pothole")) {
|
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
||||||
return "Pothole"
|
|
||||||
}
|
|
||||||
return "Signboard"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -79,12 +79,14 @@ export function DashboardMap({
|
|||||||
// Filter detections with valid GPS coordinates
|
// Filter detections with valid GPS coordinates
|
||||||
const validDetections = detections.filter(d => d.latitude && d.longitude)
|
const validDetections = detections.filter(d => d.latitude && d.longitude)
|
||||||
|
|
||||||
// Count potholes and signboards
|
// Count detections by category
|
||||||
const potholeCount = validDetections.filter(d =>
|
const counts = {
|
||||||
d.type?.toLowerCase().includes("pothole") ||
|
defected_sign_board: validDetections.filter(d => d.type?.toLowerCase() === "defected_sign_board").length,
|
||||||
d.class?.toLowerCase().includes("pothole")
|
pothole: validDetections.filter(d => d.type?.toLowerCase() === "pothole").length,
|
||||||
).length
|
road_crack: validDetections.filter(d => d.type?.toLowerCase() === "road_crack").length,
|
||||||
const signboardCount = validDetections.length - potholeCount
|
damaged_road_marking: validDetections.filter(d => d.type?.toLowerCase() === "damaged_road_marking").length,
|
||||||
|
good_sign_board: validDetections.filter(d => d.type?.toLowerCase() === "good_sign_board").length
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`overflow-hidden ${className}`}>
|
<Card className={`overflow-hidden ${className}`}>
|
||||||
@@ -105,14 +107,26 @@ export function DashboardMap({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compact Color Legend */}
|
{/* Compact Color Legend */}
|
||||||
<div className="flex items-center gap-4 text-xs">
|
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1 text-[10px] max-w-[60%]">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1">
|
||||||
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/50" />
|
<div className="w-2.5 h-2.5 rounded-full bg-[#ef4444] shadow-sm shadow-[#ef4444]/30" />
|
||||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Potholes ({potholeCount})</span>
|
<span className="text-gray-600 dark:text-gray-400 font-medium">Potholes ({counts.pothole})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1">
|
||||||
<div className="w-3 h-3 rounded-full bg-blue-500 shadow-sm shadow-blue-500/50" />
|
<div className="w-2.5 h-2.5 rounded-full bg-[#3b82f6] shadow-sm shadow-[#3b82f6]/30" />
|
||||||
<span className="text-gray-600 dark:text-gray-400 font-medium">Signboards ({signboardCount})</span>
|
<span className="text-gray-600 dark:text-gray-400 font-medium">Defected Signs ({counts.defected_sign_board})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-[#f59e0b] shadow-sm shadow-[#f59e0b]/30" />
|
||||||
|
<span className="text-gray-600 dark:text-gray-400 font-medium">Cracks ({counts.road_crack})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-[#6366f1] shadow-sm shadow-[#6366f1]/30" />
|
||||||
|
<span className="text-gray-600 dark:text-gray-400 font-medium">Markings ({counts.damaged_road_marking})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981] shadow-sm shadow-[#10b981]/30" />
|
||||||
|
<span className="text-gray-600 dark:text-gray-400 font-medium">Good Signs ({counts.good_sign_board})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,17 +4,30 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recha
|
|||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
|
|
||||||
interface DetectionDonutChartProps {
|
interface DetectionDonutChartProps {
|
||||||
potholes: number
|
defectedSignboard: number
|
||||||
signboards: number
|
pothole: number
|
||||||
|
roadCrack: number
|
||||||
|
damagedRoadMarking: number
|
||||||
|
goodSignboard: number
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
pothole: "#10b981",
|
defectedSignboard: "#3b82f6", // Blue
|
||||||
signboard: "#3b82f6"
|
pothole: "#ef4444", // Red
|
||||||
|
roadCrack: "#f59e0b", // Amber/Orange
|
||||||
|
damagedRoadMarking: "#6366f1", // Indigo
|
||||||
|
goodSignboard: "#10b981" // Emerald
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetectionDonutChart({ potholes, signboards, isLoading }: DetectionDonutChartProps) {
|
export function DetectionDonutChart({
|
||||||
|
defectedSignboard,
|
||||||
|
pothole,
|
||||||
|
roadCrack,
|
||||||
|
damagedRoadMarking,
|
||||||
|
goodSignboard,
|
||||||
|
isLoading
|
||||||
|
}: DetectionDonutChartProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-[250px] flex items-center justify-center">
|
<div className="h-[250px] flex items-center justify-center">
|
||||||
@@ -23,7 +36,7 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = potholes + signboards
|
const total = defectedSignboard + pothole + roadCrack + damagedRoadMarking + goodSignboard
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -35,18 +48,43 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ name: "Potholes", value: potholes, color: COLORS.pothole },
|
{ name: "Defected Signboards", value: defectedSignboard, color: COLORS.defectedSignboard },
|
||||||
{ name: "Signboards", value: signboards, color: COLORS.signboard }
|
{ name: "Potholes", value: pothole, color: COLORS.pothole },
|
||||||
]
|
{ name: "Road Cracks", value: roadCrack, color: COLORS.roadCrack },
|
||||||
|
{ name: "Damaged Markings", value: damagedRoadMarking, color: COLORS.damagedRoadMarking },
|
||||||
|
{ name: "Good Signboards", value: goodSignboard, color: COLORS.goodSignboard }
|
||||||
|
].filter(item => item.value > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[200px] relative">
|
<div className="h-[220px] relative">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradPothole" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#ff8a8a" />
|
||||||
|
<stop offset="100%" stopColor="#ef4444" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradDefectedSign" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#60a5fa" />
|
||||||
|
<stop offset="100%" stopColor="#3b82f6" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradCrack" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#fbbf24" />
|
||||||
|
<stop offset="100%" stopColor="#f59e0b" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradMarking" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#818cf8" />
|
||||||
|
<stop offset="100%" stopColor="#6366f1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradGoodSign" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#34d399" />
|
||||||
|
<stop offset="100%" stopColor="#10b981" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
<Pie
|
<Pie
|
||||||
data={data}
|
data={data}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="45%"
|
||||||
innerRadius={50}
|
innerRadius={50}
|
||||||
outerRadius={70}
|
outerRadius={70}
|
||||||
paddingAngle={5}
|
paddingAngle={5}
|
||||||
@@ -54,14 +92,20 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
|
|||||||
strokeWidth={0}
|
strokeWidth={0}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
>
|
>
|
||||||
{data.map((entry, index) => (
|
{data.map((entry, index) => {
|
||||||
|
const gradId = entry.name === "Potholes" ? "gradPothole" :
|
||||||
|
entry.name === "Defected Signboards" ? "gradDefectedSign" :
|
||||||
|
entry.name === "Road Cracks" ? "gradCrack" :
|
||||||
|
entry.name === "Damaged Markings" ? "gradMarking" : "gradGoodSign"
|
||||||
|
return (
|
||||||
<Cell
|
<Cell
|
||||||
key={`cell-${index}`}
|
key={`cell-${index}`}
|
||||||
fill={entry.color}
|
fill={`url(#${gradId})`}
|
||||||
fillOpacity={1}
|
fillOpacity={1}
|
||||||
className="hover:fill-opacity-80"
|
className="hover:fill-opacity-80"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={({ active, payload }) => {
|
content={({ active, payload }) => {
|
||||||
@@ -84,16 +128,16 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
|
|||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
verticalAlign="bottom"
|
verticalAlign="bottom"
|
||||||
height={36}
|
height={40}
|
||||||
content={({ payload }) => (
|
content={({ payload }) => (
|
||||||
<div className="flex items-center justify-center gap-6 mt-2">
|
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mt-2">
|
||||||
{payload?.map((entry, index) => (
|
{payload?.map((entry, index) => (
|
||||||
<div key={`legend-${index}`} className="flex items-center gap-2">
|
<div key={`legend-${index}`} className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full"
|
className="w-2.5 h-2.5 rounded-full"
|
||||||
style={{ backgroundColor: entry.color }}
|
style={{ backgroundColor: entry.color }}
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
|
||||||
{entry.value}: {data[index].value}
|
{entry.value}: {data[index].value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +148,7 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
|
|||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
{/* Center label */}
|
{/* Center label */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-40px' }}>
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none" style={{ marginTop: '-55px' }}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
|
<p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
|
||||||
<p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>
|
<p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface GradientStatsCardProps {
|
|||||||
subtitle?: string
|
subtitle?: string
|
||||||
value: number | string
|
value: number | string
|
||||||
icon: LucideIcon
|
icon: LucideIcon
|
||||||
gradient: "green" | "coral" | "blue" | "purple"
|
gradient: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +36,24 @@ const gradientStyles = {
|
|||||||
border: "border-l-4 border-l-[var(--border)]",
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
background: "bg-card",
|
||||||
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
|
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||||
|
},
|
||||||
|
indigo: {
|
||||||
|
background: "bg-card",
|
||||||
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
|
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||||
|
},
|
||||||
|
emerald: {
|
||||||
|
background: "bg-card",
|
||||||
|
border: "border-l-4 border-l-[var(--border)]",
|
||||||
|
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||||
|
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import { Loader2 } from "lucide-react"
|
|||||||
|
|
||||||
interface LocationData {
|
interface LocationData {
|
||||||
name: string
|
name: string
|
||||||
potholes: number
|
defected_sign_board: number
|
||||||
signboards: number
|
pothole: number
|
||||||
|
road_crack: number
|
||||||
|
damaged_road_marking: number
|
||||||
|
good_sign_board: number
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +19,11 @@ interface LocationBarChartProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
pothole: "#10b981",
|
defected_sign_board: "#60a5fa", // Lighter Blue
|
||||||
signboard: "#3b82f6"
|
pothole: "#ff8a8a", // Lighter Red
|
||||||
|
road_crack: "#fbbf24", // Lighter Orange
|
||||||
|
damaged_road_marking: "#818cf8", // Lighter Indigo
|
||||||
|
good_sign_board: "#34d399" // Lighter Emerald
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
||||||
@@ -44,16 +50,29 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
|||||||
<BarChart
|
<BarChart
|
||||||
data={data}
|
data={data}
|
||||||
margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
|
margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
|
||||||
barCategoryGap="25%"
|
barCategoryGap="10%"
|
||||||
|
barGap={2}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="barGradientPothole" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="barPothole" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stopColor="#10b981" stopOpacity={0.8} />
|
<stop offset="0%" stopColor="#ff8a8a" />
|
||||||
<stop offset="100%" stopColor="#10b981" stopOpacity={0.4} />
|
<stop offset="100%" stopColor="#ef4444" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="barGradientSignboard" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="barDefectedSign" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.8} />
|
<stop offset="0%" stopColor="#60a5fa" />
|
||||||
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0.4} />
|
<stop offset="100%" stopColor="#3b82f6" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="barCrack" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#fbbf24" />
|
||||||
|
<stop offset="100%" stopColor="#f59e0b" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="barMarking" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#818cf8" />
|
||||||
|
<stop offset="100%" stopColor="#6366f1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="barGoodSign" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#34d399" />
|
||||||
|
<stop offset="100%" stopColor="#10b981" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
@@ -89,7 +108,7 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
|||||||
className="w-2 h-2 rounded-full"
|
className="w-2 h-2 rounded-full"
|
||||||
style={{ backgroundColor: entry.color }}
|
style={{ backgroundColor: entry.color }}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground">{entry.name}:</span>
|
<span className="text-muted-foreground">{(entry.name as string).replace(/_/g, ' ')}:</span>
|
||||||
<span className="font-semibold">{entry.value}</span>
|
<span className="font-semibold">{entry.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -103,15 +122,15 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
|||||||
verticalAlign="top"
|
verticalAlign="top"
|
||||||
height={30}
|
height={30}
|
||||||
content={({ payload }) => (
|
content={({ payload }) => (
|
||||||
<div className="flex items-center justify-center gap-6 mb-2">
|
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 mb-2">
|
||||||
{payload?.map((entry, index) => (
|
{payload?.map((entry, index) => (
|
||||||
<div key={`legend-${index}`} className="flex items-center gap-2">
|
<div key={`legend-${index}`} className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-sm"
|
className="w-2.5 h-2.5 rounded-sm"
|
||||||
style={{ backgroundColor: entry.color }}
|
style={{ backgroundColor: entry.color }}
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
|
||||||
{entry.value}
|
{(entry.value as string).replace(/_/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -119,16 +138,37 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="potholes"
|
dataKey="pothole"
|
||||||
name="Potholes"
|
name="Pothole"
|
||||||
fill="url(#barGradientPothole)"
|
fill="url(#barPothole)"
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="signboards"
|
dataKey="defected_sign_board"
|
||||||
name="Signboards"
|
name="Defected Signboard"
|
||||||
fill="url(#barGradientSignboard)"
|
fill="url(#barDefectedSign)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="road_crack"
|
||||||
|
name="Road Crack"
|
||||||
|
fill="url(#barCrack)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="damaged_road_marking"
|
||||||
|
name="Damaged Marking"
|
||||||
|
fill="url(#barMarking)"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="good_sign_board"
|
||||||
|
name="Good Signboard"
|
||||||
|
fill="url(#barGoodSign)"
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -118,9 +118,26 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
|
|||||||
|
|
||||||
{/* Detection markers */}
|
{/* Detection markers */}
|
||||||
{validDetections.map((detection, idx) => {
|
{validDetections.map((detection, idx) => {
|
||||||
const isDetPothole = detection.type === "pothole"
|
const type = (detection.type || "").toLowerCase()
|
||||||
const markerColor = isDetPothole ? "#ef4444" : "#3b82f6"
|
let markerColor = "#64748b" // Default Slate
|
||||||
const strokeColor = isDetPothole ? "#991b1b" : "#1e40af"
|
let strokeColor = "#475569"
|
||||||
|
|
||||||
|
if (type === "pothole") {
|
||||||
|
markerColor = "#ef4444"
|
||||||
|
strokeColor = "#b91c1c"
|
||||||
|
} else if (type === "defected_sign_board") {
|
||||||
|
markerColor = "#3b82f6"
|
||||||
|
strokeColor = "#1d4ed8"
|
||||||
|
} else if (type === "road_crack") {
|
||||||
|
markerColor = "#f59e0b"
|
||||||
|
strokeColor = "#b45309"
|
||||||
|
} else if (type === "damaged_road_marking") {
|
||||||
|
markerColor = "#6366f1"
|
||||||
|
strokeColor = "#4338ca"
|
||||||
|
} else if (type === "good_sign_board") {
|
||||||
|
markerColor = "#10b981"
|
||||||
|
strokeColor = "#047857"
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CircleMarker
|
<CircleMarker
|
||||||
@@ -135,8 +152,8 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
|
|||||||
>
|
>
|
||||||
<Popup>
|
<Popup>
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<div className="font-semibold">
|
<div className="font-semibold capitalize">
|
||||||
{isDetPothole ? "Pothole" : (detection.class || detection.type || "").replace(/_/g, " ")} #{detection.id}
|
{(detection.type || "").replace(/_/g, " ")} #{detection.id}
|
||||||
</div>
|
</div>
|
||||||
<div>Frame: {detection.frame_number}</div>
|
<div>Frame: {detection.frame_number}</div>
|
||||||
<div>Confidence: {(detection.confidence * 100).toFixed(1)}%</div>
|
<div>Confidence: {(detection.confidence * 100).toFixed(1)}%</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Target, AlertTriangle, Film, Activity, Gauge, Monitor } from "lucide-react"
|
import { Target, AlertTriangle, Film, Activity, Gauge, Monitor, SignpostBig } from "lucide-react"
|
||||||
import type { DetectionData } from "@/lib/types"
|
import type { DetectionData } from "@/lib/types"
|
||||||
|
|
||||||
type SummarySectionProps = {
|
type SummarySectionProps = {
|
||||||
@@ -10,26 +10,47 @@ type SummarySectionProps = {
|
|||||||
|
|
||||||
export function SummarySection({ data }: SummarySectionProps) {
|
export function SummarySection({ data }: SummarySectionProps) {
|
||||||
const stats = [
|
const stats = [
|
||||||
|
{
|
||||||
|
label: "Total Road Damage",
|
||||||
|
value: data.summary.total_road_damage || 0,
|
||||||
|
icon: Target,
|
||||||
|
color: "text-purple-500",
|
||||||
|
bgColor: "bg-purple-50 dark:bg-purple-950/30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Defected Signboards",
|
||||||
|
value: data.summary.unique_defected_sign_board || 0,
|
||||||
|
icon: SignpostBig,
|
||||||
|
color: "text-blue-500",
|
||||||
|
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Unique Potholes",
|
label: "Unique Potholes",
|
||||||
value: data.summary.unique_potholes || 0,
|
value: data.summary.unique_pothole || 0,
|
||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
color: "text-red-500",
|
color: "text-red-500",
|
||||||
bgColor: "bg-red-50 dark:bg-red-950/30",
|
bgColor: "bg-red-50 dark:bg-red-950/30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Total Detections",
|
label: "Road Cracks",
|
||||||
value: data.summary.total_detections || 0,
|
value: data.summary.unique_road_crack || 0,
|
||||||
icon: Target,
|
icon: AlertTriangle,
|
||||||
color: "text-blue-500",
|
color: "text-orange-500",
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
bgColor: "bg-orange-50 dark:bg-orange-950/30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Total Frames",
|
label: "Damaged Markings",
|
||||||
value: data.summary.total_frames || data.video_info.total_frames,
|
value: data.summary.unique_damaged_road_marking || 0,
|
||||||
icon: Film,
|
icon: Activity,
|
||||||
color: "text-purple-500",
|
color: "text-indigo-500",
|
||||||
bgColor: "bg-purple-50 dark:bg-purple-950/30",
|
bgColor: "bg-indigo-50 dark:bg-indigo-950/30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Good Signboards",
|
||||||
|
value: data.summary.unique_good_sign_board || 0,
|
||||||
|
icon: SignpostBig,
|
||||||
|
color: "text-emerald-500",
|
||||||
|
bgColor: "bg-emerald-50 dark:bg-emerald-950/30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Detection Rate",
|
label: "Detection Rate",
|
||||||
@@ -52,28 +73,35 @@ export function SummarySection({ data }: SummarySectionProps) {
|
|||||||
color: "text-blue-500",
|
color: "text-blue-500",
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Total Frames",
|
||||||
|
value: data.summary.total_frames || data.video_info.total_frames,
|
||||||
|
icon: Film,
|
||||||
|
color: "text-purple-500",
|
||||||
|
bgColor: "bg-purple-50 dark:bg-purple-950/30",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Detection Summary</CardTitle>
|
<CardTitle>Detection Summary</CardTitle>
|
||||||
<CardDescription>Overview of pothole detection results</CardDescription>
|
<CardDescription>Overview of road analysis results</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
{stats.map((stat, index) => {
|
{stats.map((stat, index) => {
|
||||||
const Icon = stat.icon
|
const Icon = stat.icon
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={stat.label}
|
key={stat.label}
|
||||||
className="flex flex-col items-center justify-center p-4 rounded-lg"
|
className="flex flex-col items-center justify-center p-4 rounded-lg border border-border/50 bg-card hover:shadow-md transition-shadow"
|
||||||
>
|
>
|
||||||
<div className={`${stat.bgColor} p-3 rounded-full mb-3`}>
|
<div className={`${stat.bgColor} p-3 rounded-full mb-3`}>
|
||||||
<Icon className={`h-5 w-5 ${stat.color}`} />
|
<Icon className={`h-5 w-5 ${stat.color}`} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-3xl font-bold ${stat.color} mb-1`}>{stat.value}</div>
|
<div className={`text-xl font-bold ${stat.color} mb-1`}>{stat.value}</div>
|
||||||
<div className="text-xs text-muted-foreground text-center">{stat.label}</div>
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground text-center">{stat.label}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -283,33 +283,47 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
|
|||||||
const isSignboard = detectionType === "sign-board-detection" || isCombined
|
const isSignboard = detectionType === "sign-board-detection" || isCombined
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
...(isPothole ? [{
|
|
||||||
label: "Unique Potholes",
|
|
||||||
value: data.summary.unique_potholes || 0,
|
|
||||||
icon: AlertTriangle,
|
|
||||||
color: "text-red-500",
|
|
||||||
bgColor: "bg-red-50 dark:bg-red-950/30",
|
|
||||||
}] : []),
|
|
||||||
...(isSignboard ? [{
|
|
||||||
label: "Unique Signboards",
|
|
||||||
value: data.summary.unique_signboards || 0,
|
|
||||||
icon: SignpostBig,
|
|
||||||
color: "text-blue-500",
|
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
|
||||||
}] : []),
|
|
||||||
{
|
{
|
||||||
label: "Total Detections",
|
label: "Total Road Damage",
|
||||||
value: data.summary.total_detections || 0,
|
value: data.summary.total_road_damage || 0,
|
||||||
icon: Target,
|
icon: Target,
|
||||||
|
color: "text-purple-500",
|
||||||
|
bgColor: "bg-purple-50 dark:bg-purple-950/30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Defected Signboards",
|
||||||
|
value: data.summary.unique_defected_sign_board || 0,
|
||||||
|
icon: SignpostBig,
|
||||||
color: "text-blue-500",
|
color: "text-blue-500",
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Total Frames",
|
label: "Unique Potholes",
|
||||||
value: data.summary.total_frames || data.video_info.total_frames,
|
value: data.summary.unique_pothole || 0,
|
||||||
icon: Film,
|
icon: AlertTriangle,
|
||||||
color: "text-purple-500",
|
color: "text-red-500",
|
||||||
bgColor: "bg-purple-50 dark:bg-purple-950/30",
|
bgColor: "bg-red-50 dark:bg-red-950/30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Road Cracks",
|
||||||
|
value: data.summary.unique_road_crack || 0,
|
||||||
|
icon: AlertTriangle,
|
||||||
|
color: "text-orange-500",
|
||||||
|
bgColor: "bg-orange-50 dark:bg-orange-950/30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Damaged Markings",
|
||||||
|
value: data.summary.unique_damaged_road_marking || 0,
|
||||||
|
icon: Activity,
|
||||||
|
color: "text-indigo-500",
|
||||||
|
bgColor: "bg-indigo-50 dark:bg-indigo-950/30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Good Signboards",
|
||||||
|
value: data.summary.unique_good_sign_board || 0,
|
||||||
|
icon: SignpostBig,
|
||||||
|
color: "text-emerald-500",
|
||||||
|
bgColor: "bg-emerald-50 dark:bg-emerald-950/30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Detection Rate",
|
label: "Detection Rate",
|
||||||
@@ -332,6 +346,13 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
|
|||||||
color: "text-blue-500",
|
color: "text-blue-500",
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
bgColor: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Total Frames",
|
||||||
|
value: data.summary.total_frames || data.video_info.total_frames,
|
||||||
|
icon: Film,
|
||||||
|
color: "text-purple-500",
|
||||||
|
bgColor: "bg-purple-50 dark:bg-purple-950/30",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -356,7 +377,7 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className={`grid gap-3 ${isCombined ? 'grid-cols-3 md:grid-cols-7' : 'grid-cols-3 md:grid-cols-6'}`}>
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||||
{stats.map((stat, index) => {
|
{stats.map((stat, index) => {
|
||||||
const Icon = stat.icon
|
const Icon = stat.icon
|
||||||
return (
|
return (
|
||||||
@@ -526,12 +547,19 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection
|
|||||||
const width = x2 - x1
|
const width = x2 - x1
|
||||||
const height = y2 - y1
|
const height = y2 - y1
|
||||||
|
|
||||||
// Determine if this specific detection is a pothole or signboard
|
// Map individual detection types to colors
|
||||||
const isDetPothole = detection._detType === 'pothole' || detection.type === 'pothole' || (detection.pothole_id !== undefined && !detection.signboard_id)
|
const type = (detection.type || detection._detType || '').toLowerCase()
|
||||||
|
const detectionColors: Record<string, string> = {
|
||||||
|
'pothole': '#ef4444',
|
||||||
|
'defected_sign_board': '#3b82f6',
|
||||||
|
'road_crack': '#f59e0b',
|
||||||
|
'damaged_road_marking': '#6366f1',
|
||||||
|
'good_sign_board': '#10b981',
|
||||||
|
'signboard': '#3b82f6' // fallback
|
||||||
|
}
|
||||||
|
|
||||||
// Set colors based on individual detection type
|
const boxColor = detectionColors[type] || '#3b82f6'
|
||||||
const boxColor = isDetPothole ? '#ef4444' : '#3b82f6' // red for potholes, blue for signboards
|
const textBgColor = boxColor + 'e6' // Adding alpha
|
||||||
const textBgColor = isDetPothole ? 'rgba(239, 68, 68, 0.9)' : 'rgba(59, 130, 246, 0.9)'
|
|
||||||
|
|
||||||
// Draw bounding box
|
// Draw bounding box
|
||||||
ctx.strokeStyle = boxColor
|
ctx.strokeStyle = boxColor
|
||||||
@@ -539,13 +567,13 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection
|
|||||||
ctx.strokeRect(x1, y1, width, height)
|
ctx.strokeRect(x1, y1, width, height)
|
||||||
|
|
||||||
// Draw semi-transparent fill
|
// Draw semi-transparent fill
|
||||||
ctx.fillStyle = isDetPothole ? 'rgba(239, 68, 68, 0.15)' : 'rgba(59, 130, 246, 0.15)'
|
ctx.fillStyle = type === 'pothole' ? 'rgba(239, 68, 68, 0.15)' : 'rgba(59, 130, 246, 0.15)'
|
||||||
ctx.fillRect(x1, y1, width, height)
|
ctx.fillRect(x1, y1, width, height)
|
||||||
|
|
||||||
// Prepare label text
|
// Prepare label text
|
||||||
const id = detection.pothole_id ?? detection.signboard_id ?? detection.detection_id
|
const id = detection.pothole_id ?? detection.signboard_id ?? detection.detection_id
|
||||||
const confidence = (detection.confidence * 100).toFixed(1)
|
const confidence = (detection.confidence * 100).toFixed(1)
|
||||||
let labelText = isDetPothole
|
let labelText = type === 'pothole'
|
||||||
? `Pothole #${id}`
|
? `Pothole #${id}`
|
||||||
: `${(detection.type || 'Sign').replace(/_/g, ' ')} #${id}`
|
: `${(detection.type || 'Sign').replace(/_/g, ' ')} #${id}`
|
||||||
labelText += ` ${confidence}%`
|
labelText += ` ${confidence}%`
|
||||||
|
|||||||
37
lib/api.ts
37
lib/api.ts
@@ -278,8 +278,12 @@ export interface Video {
|
|||||||
filename: string
|
filename: string
|
||||||
detection_type: "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
|
detection_type: "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
|
||||||
status: "pending" | "processing" | "completed" | "failed"
|
status: "pending" | "processing" | "completed" | "failed"
|
||||||
unique_potholes?: number
|
unique_defected_sign_board?: number
|
||||||
unique_signboards?: number
|
unique_pothole?: number
|
||||||
|
unique_road_crack?: number
|
||||||
|
unique_damaged_road_marking?: number
|
||||||
|
unique_good_sign_board?: number
|
||||||
|
total_road_damage?: number
|
||||||
total_detections?: number
|
total_detections?: number
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@@ -289,16 +293,35 @@ export interface Video {
|
|||||||
* Fetch all videos (for dashboard)
|
* Fetch all videos (for dashboard)
|
||||||
*/
|
*/
|
||||||
export async function fetchVideos(): Promise<Video[]> {
|
export async function fetchVideos(): Promise<Video[]> {
|
||||||
const response = await apiRequest<{ videos: Array<{ video_id: string; status: string; progress: number; summary?: { unique_potholes?: number; unique_signboards?: number; total_detections?: number } }> }>("/videos")
|
const response = await apiRequest<{
|
||||||
|
videos: Array<{
|
||||||
|
video_id: string;
|
||||||
|
status: string;
|
||||||
|
progress: number;
|
||||||
|
summary?: {
|
||||||
|
unique_defected_sign_board?: number;
|
||||||
|
unique_pothole?: number;
|
||||||
|
unique_road_crack?: number;
|
||||||
|
unique_damaged_road_marking?: number;
|
||||||
|
unique_good_sign_board?: number;
|
||||||
|
total_road_damage?: number;
|
||||||
|
total_detections?: number;
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}>("/videos")
|
||||||
|
|
||||||
// Transform the response to match our Video interface
|
// Transform the response to match our Video interface
|
||||||
return response.videos.map(v => ({
|
return response.videos.map(v => ({
|
||||||
id: v.video_id,
|
id: v.video_id,
|
||||||
filename: v.video_id, // Using video_id as filename since backend doesn't provide it
|
filename: v.video_id,
|
||||||
detection_type: (v.summary?.unique_potholes !== undefined && v.summary?.unique_signboards !== undefined) ? "pot-sign-detection" as const : v.summary?.unique_potholes !== undefined ? "pothole-detection" as const : "sign-board-detection" as const,
|
detection_type: "pot-sign-detection" as const,
|
||||||
status: v.status as Video["status"],
|
status: v.status as Video["status"],
|
||||||
unique_potholes: v.summary?.unique_potholes,
|
unique_defected_sign_board: v.summary?.unique_defected_sign_board,
|
||||||
unique_signboards: v.summary?.unique_signboards,
|
unique_pothole: v.summary?.unique_pothole,
|
||||||
|
unique_road_crack: v.summary?.unique_road_crack,
|
||||||
|
unique_damaged_road_marking: v.summary?.unique_damaged_road_marking,
|
||||||
|
unique_good_sign_board: v.summary?.unique_good_sign_board,
|
||||||
|
total_road_damage: v.summary?.total_road_damage,
|
||||||
total_detections: v.summary?.total_detections,
|
total_detections: v.summary?.total_detections,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
|
|||||||
49
lib/project-service.ts
Normal file
49
lib/project-service.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type Project,
|
||||||
|
type Package,
|
||||||
|
type Location,
|
||||||
|
type Video,
|
||||||
|
type Detection
|
||||||
|
} from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle data extraction from project summaries
|
||||||
|
*/
|
||||||
|
export const projectDataService = {
|
||||||
|
/**
|
||||||
|
* Extracts all detections from a project summary, optionally filtered by package and location
|
||||||
|
*/
|
||||||
|
extractDetections(
|
||||||
|
projectSummary: any,
|
||||||
|
selectedPackageId?: string | null,
|
||||||
|
selectedLocationId?: string | null
|
||||||
|
): Detection[] {
|
||||||
|
if (!projectSummary) return []
|
||||||
|
|
||||||
|
const detections: Detection[] = []
|
||||||
|
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
|
||||||
|
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
|
||||||
|
: projectSummary.packages || {}
|
||||||
|
|
||||||
|
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
|
||||||
|
const locationsToProcess = selectedLocationId && selectedLocationId !== "all"
|
||||||
|
? { [selectedLocationId]: (pkg as any).locations[selectedLocationId] }
|
||||||
|
: (pkg as any).locations || {}
|
||||||
|
|
||||||
|
for (const [locName, loc] of Object.entries(locationsToProcess)) {
|
||||||
|
if (!loc) continue
|
||||||
|
const locationDetections = (loc as any).detections || []
|
||||||
|
detections.push(...locationDetections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API service for project and video data
|
||||||
|
*/
|
||||||
|
export * from "./api"
|
||||||
49
lib/types.ts
49
lib/types.ts
@@ -1,5 +1,18 @@
|
|||||||
export type DetectionType = "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
|
export type DetectionType = "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
|
||||||
|
|
||||||
|
export interface DetectionListItem {
|
||||||
|
detection_id?: number
|
||||||
|
pothole_id?: number
|
||||||
|
signboard_id?: number
|
||||||
|
type: string
|
||||||
|
first_detected_frame: number
|
||||||
|
first_detected_time: number
|
||||||
|
confidence: number
|
||||||
|
bbox?: { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
lat?: number
|
||||||
|
lng?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type DetectionData = {
|
export type DetectionData = {
|
||||||
video_id: string
|
video_id: string
|
||||||
detection_type?: string
|
detection_type?: string
|
||||||
@@ -11,34 +24,22 @@ export type DetectionData = {
|
|||||||
total_frames: number
|
total_frames: number
|
||||||
}
|
}
|
||||||
summary: {
|
summary: {
|
||||||
unique_potholes?: number
|
unique_defected_sign_board?: number
|
||||||
unique_signboards?: number
|
unique_pothole?: number
|
||||||
|
unique_road_crack?: number
|
||||||
|
unique_damaged_road_marking?: number
|
||||||
|
unique_good_sign_board?: number
|
||||||
|
total_road_damage?: number
|
||||||
total_detections: number
|
total_detections: number
|
||||||
total_frames: number
|
total_frames: number
|
||||||
detection_rate: number
|
detection_rate: number
|
||||||
}
|
}
|
||||||
pothole_list?: Array<{
|
pothole_list?: Array<DetectionListItem>
|
||||||
pothole_id?: number
|
defected_sign_board_list?: Array<DetectionListItem>
|
||||||
detection_id?: number
|
road_crack_list?: Array<DetectionListItem>
|
||||||
type?: string
|
damaged_road_marking_list?: Array<DetectionListItem>
|
||||||
first_detected_frame: number
|
good_sign_board_list?: Array<DetectionListItem>
|
||||||
first_detected_time: number
|
signboard_list?: Array<DetectionListItem> // Keeping for backward compatibility
|
||||||
confidence: number
|
|
||||||
bbox?: { x1: number; y1: number; x2: number; y2: number }
|
|
||||||
lat?: number
|
|
||||||
lng?: number
|
|
||||||
}>
|
|
||||||
signboard_list?: Array<{
|
|
||||||
signboard_id?: number
|
|
||||||
detection_id?: number
|
|
||||||
type: string
|
|
||||||
first_detected_frame: number
|
|
||||||
first_detected_time: number
|
|
||||||
confidence: number
|
|
||||||
bbox?: { x1: number; y1: number; x2: number; y2: number }
|
|
||||||
lat?: number
|
|
||||||
lng?: number
|
|
||||||
}>
|
|
||||||
frames: Array<{
|
frames: Array<{
|
||||||
frame_id: number
|
frame_id: number
|
||||||
// Legacy format: separate arrays
|
// Legacy format: separate arrays
|
||||||
|
|||||||
Reference in New Issue
Block a user