added more feature according to updated model

This commit is contained in:
sumona-banerjeee
2026-02-13 18:56:35 +05:30
parent 265df337cd
commit 99cab7c287
13 changed files with 588 additions and 206 deletions

View File

@@ -55,57 +55,102 @@ interface ProjectSummary {
}
interface DetectionStats {
totalPotholes: number
totalSignboards: number
totalDetections: number
totalDefectedSignboard: number
totalPothole: number
totalRoadCrack: number
totalDamagedRoadMarking: number
totalGoodSignboard: number
totalRoadDamage: number
locationData: Array<{
name: string
potholes: number
signboards: number
defected_sign_board: number
pothole: number
road_crack: number
damaged_road_marking: number
good_sign_board: number
total: number
}>
}
function calculateStats(summary: ProjectSummary | null): DetectionStats {
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 totalSignboards = 0
let totalDefectedSignboard = 0
let totalPothole = 0
let totalRoadCrack = 0
let totalDamagedRoadMarking = 0
let totalGoodSignboard = 0
let totalRoadDamage = 0
const locationData: DetectionStats["locationData"] = []
for (const pkg of Object.values(summary.packages || {})) {
for (const [locName, loc] of Object.entries(pkg.locations || {})) {
let locPotholes = 0
let locSignboards = 0
let locDefectedSignboard = 0
let locPothole = 0
let locRoadCrack = 0
let locDamagedRoadMarking = 0
let locGoodSignboard = 0
for (const detection of loc.detections || []) {
if (detection.type.toLowerCase().includes("pothole")) {
locPotholes++
totalPotholes++
} else {
locSignboards++
totalSignboards++
const type = detection.type.toLowerCase()
if (type === "defected_sign_board") {
locDefectedSignboard++
totalDefectedSignboard++
} else if (type === "pothole") {
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
locationData.push({
name: shortName,
potholes: locPotholes,
signboards: locSignboards,
total: locPotholes + locSignboards
defected_sign_board: locDefectedSignboard,
pothole: locPothole,
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 {
totalPotholes,
totalSignboards,
totalDetections: totalPotholes + totalSignboards,
totalDefectedSignboard,
totalPothole,
totalRoadCrack,
totalDamagedRoadMarking,
totalGoodSignboard,
totalRoadDamage,
locationData
}
}
@@ -116,9 +161,12 @@ export default function DashboardPage() {
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
const [projectSummary, setProjectSummary] = useState<ProjectSummary | null>(null)
const [stats, setStats] = useState<DetectionStats>({
totalPotholes: 0,
totalSignboards: 0,
totalDetections: 0,
totalDefectedSignboard: 0,
totalPothole: 0,
totalRoadCrack: 0,
totalDamagedRoadMarking: 0,
totalGoodSignboard: 0,
totalRoadDamage: 0,
locationData: []
})
const [error, setError] = useState<string | null>(null)
@@ -218,12 +266,23 @@ export default function DashboardPage() {
// Filter stats based on selections
useEffect(() => {
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
}
let totalPotholes = 0
let totalSignboards = 0
let totalDefectedSignboard = 0
let totalPothole = 0
let totalRoadCrack = 0
let totalDamagedRoadMarking = 0
let totalGoodSignboard = 0
const locationData: DetectionStats["locationData"] = []
const packagesToProcess = selectedPackageId && selectedPackageId !== "all"
@@ -238,35 +297,54 @@ export default function DashboardPage() {
for (const [locName, loc] of Object.entries(locationsToProcess)) {
if (!loc) continue
let locPotholes = 0
let locSignboards = 0
let locDefectedSignboard = 0
let locPothole = 0
let locRoadCrack = 0
let locDamagedRoadMarking = 0
let locGoodSignboard = 0
for (const detection of loc.detections || []) {
if (detection.type.toLowerCase().includes("pothole")) {
locPotholes++
totalPotholes++
} else {
locSignboards++
totalSignboards++
const type = detection.type.toLowerCase()
if (type === "defected_sign_board") {
locDefectedSignboard++
totalDefectedSignboard++
} else if (type === "pothole") {
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
locationData.push({
name: shortName,
potholes: locPotholes,
signboards: locSignboards,
total: locPotholes + locSignboards
defected_sign_board: locDefectedSignboard,
pothole: locPothole,
road_crack: locRoadCrack,
damaged_road_marking: locDamagedRoadMarking,
good_sign_board: locGoodSignboard,
total: locDefectedSignboard + locPothole + locRoadCrack + locDamagedRoadMarking + locGoodSignboard
})
}
}
}
setStats({
totalPotholes,
totalSignboards,
totalDetections: totalPotholes + totalSignboards,
totalDefectedSignboard,
totalPothole,
totalRoadCrack,
totalDamagedRoadMarking,
totalGoodSignboard,
totalRoadDamage: totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking,
locationData
})
}, [projectSummary, selectedPackageId, selectedLocationId])
@@ -311,29 +389,53 @@ export default function DashboardPage() {
{/* Stats Cards - Top Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<GradientStatsCard
title="Total Detections"
subtitle="All detected objects"
value={stats.totalDetections}
title="Total Road Damage"
subtitle="Combined damage detections"
value={stats.totalRoadDamage}
icon={TrendingUp}
gradient="purple"
isLoading={isLoading}
/>
<GradientStatsCard
title="Potholes"
subtitle="Road surface damage"
value={stats.totalPotholes}
subtitle="Surface depressions"
value={stats.totalPothole}
icon={AlertTriangle}
gradient="green"
isLoading={isLoading}
/>
<GradientStatsCard
title="Signboards"
subtitle="Traffic signs detected"
value={stats.totalSignboards}
title="Defected Signboards"
subtitle="Damaged traffic signs"
value={stats.totalDefectedSignboard}
icon={RectangleHorizontal}
gradient="blue"
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>
{/* Filter Selector - Above main content */}
@@ -368,8 +470,11 @@ export default function DashboardPage() {
</CardHeader>
<CardContent className="pt-4">
<DetectionDonutChart
potholes={stats.totalPotholes}
signboards={stats.totalSignboards}
defectedSignboard={stats.totalDefectedSignboard}
pothole={stats.totalPothole}
roadCrack={stats.totalRoadCrack}
damagedRoadMarking={stats.totalDamagedRoadMarking}
goodSignboard={stats.totalGoodSignboard}
isLoading={isLoading}
/>
</CardContent>
@@ -389,7 +494,15 @@ export default function DashboardPage() {
</CardHeader>
<CardContent className="pt-4">
<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}
/>
</CardContent>

View File

@@ -175,6 +175,7 @@
html {
scrollbar-gutter: stable;
scroll-behavior: smooth;
}
body[data-scroll-locked] {

View File

@@ -53,18 +53,24 @@ export default function DashboardMapContent({ detections }: DashboardMapContentP
// Get marker color based on detection type
const getMarkerColor = (type: string) => {
if (type.toLowerCase().includes("pothole")) {
return { fill: "#10b981", stroke: "#065f46" } // Green for potholes
const t = type.toLowerCase()
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
const getTypeName = (type: string) => {
if (type.toLowerCase().includes("pothole")) {
return "Pothole"
}
return "Signboard"
return type.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}
return (

View File

@@ -79,12 +79,14 @@ export function DashboardMap({
// Filter detections with valid GPS coordinates
const validDetections = detections.filter(d => d.latitude && d.longitude)
// Count potholes and signboards
const potholeCount = validDetections.filter(d =>
d.type?.toLowerCase().includes("pothole") ||
d.class?.toLowerCase().includes("pothole")
).length
const signboardCount = validDetections.length - potholeCount
// Count detections by category
const counts = {
defected_sign_board: validDetections.filter(d => d.type?.toLowerCase() === "defected_sign_board").length,
pothole: validDetections.filter(d => d.type?.toLowerCase() === "pothole").length,
road_crack: validDetections.filter(d => d.type?.toLowerCase() === "road_crack").length,
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 (
<Card className={`overflow-hidden ${className}`}>
@@ -105,14 +107,26 @@ export function DashboardMap({
</div>
{/* Compact Color Legend */}
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/50" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Potholes ({potholeCount})</span>
<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">
<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 ({counts.pothole})</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-blue-500 shadow-sm shadow-blue-500/50" />
<span className="text-gray-600 dark:text-gray-400 font-medium">Signboards ({signboardCount})</span>
<div className="flex items-center gap-1">
<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">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>

View File

@@ -4,17 +4,30 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recha
import { Loader2 } from "lucide-react"
interface DetectionDonutChartProps {
potholes: number
signboards: number
defectedSignboard: number
pothole: number
roadCrack: number
damagedRoadMarking: number
goodSignboard: number
isLoading: boolean
}
const COLORS = {
pothole: "#10b981",
signboard: "#3b82f6"
defectedSignboard: "#3b82f6", // Blue
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) {
return (
<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) {
return (
@@ -35,18 +48,43 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
}
const data = [
{ name: "Potholes", value: potholes, color: COLORS.pothole },
{ name: "Signboards", value: signboards, color: COLORS.signboard }
]
{ name: "Defected Signboards", value: defectedSignboard, color: COLORS.defectedSignboard },
{ 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 (
<div className="h-[200px] relative">
<div className="h-[220px] relative">
<ResponsiveContainer width="100%" height="100%">
<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
data={data}
cx="50%"
cy="50%"
cy="45%"
innerRadius={50}
outerRadius={70}
paddingAngle={5}
@@ -54,14 +92,20 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
strokeWidth={0}
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
key={`cell-${index}`}
fill={entry.color}
fill={`url(#${gradId})`}
fillOpacity={1}
className="hover:fill-opacity-80"
/>
))}
)
})}
</Pie>
<Tooltip
content={({ active, payload }) => {
@@ -84,16 +128,16 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
/>
<Legend
verticalAlign="bottom"
height={36}
height={40}
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) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div key={`legend-${index}`} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-full"
className="w-2.5 h-2.5 rounded-full"
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}
</span>
</div>
@@ -104,7 +148,7 @@ export function DetectionDonutChart({ potholes, signboards, isLoading }: Detecti
</PieChart>
</ResponsiveContainer>
{/* 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">
<p className="text-xl font-extrabold text-[#2563eb]">{total}</p>
<p className="text-[9px] uppercase tracking-wider text-muted-foreground">Total</p>

View File

@@ -8,7 +8,7 @@ interface GradientStatsCardProps {
subtitle?: string
value: number | string
icon: LucideIcon
gradient: "green" | "coral" | "blue" | "purple"
gradient: "green" | "coral" | "blue" | "purple" | "orange" | "indigo" | "emerald"
isLoading?: boolean
}
@@ -36,6 +36,24 @@ const gradientStyles = {
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"
},
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"
}
}

View File

@@ -5,8 +5,11 @@ import { Loader2 } from "lucide-react"
interface LocationData {
name: string
potholes: number
signboards: number
defected_sign_board: number
pothole: number
road_crack: number
damaged_road_marking: number
good_sign_board: number
total: number
}
@@ -16,8 +19,11 @@ interface LocationBarChartProps {
}
const COLORS = {
pothole: "#10b981",
signboard: "#3b82f6"
defected_sign_board: "#60a5fa", // Lighter Blue
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) {
@@ -44,16 +50,29 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
<BarChart
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 20 }}
barCategoryGap="25%"
barCategoryGap="10%"
barGap={2}
>
<defs>
<linearGradient id="barGradientPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#10b981" stopOpacity={0.8} />
<stop offset="100%" stopColor="#10b981" stopOpacity={0.4} />
<linearGradient id="barPothole" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ff8a8a" />
<stop offset="100%" stopColor="#ef4444" />
</linearGradient>
<linearGradient id="barGradientSignboard" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.8} />
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0.4} />
<linearGradient id="barDefectedSign" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" />
<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>
</defs>
<CartesianGrid
@@ -89,7 +108,7 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
className="w-2 h-2 rounded-full"
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>
</div>
))}
@@ -103,15 +122,15 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
verticalAlign="top"
height={30}
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) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div key={`legend-${index}`} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-sm"
className="w-2.5 h-2.5 rounded-sm"
style={{ backgroundColor: entry.color }}
/>
<span className="text-[10px] text-muted-foreground">
{entry.value}
<span className="text-[9px] text-muted-foreground whitespace-nowrap">
{(entry.value as string).replace(/_/g, ' ')}
</span>
</div>
))}
@@ -119,16 +138,37 @@ export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
)}
/>
<Bar
dataKey="potholes"
name="Potholes"
fill="url(#barGradientPothole)"
dataKey="pothole"
name="Pothole"
fill="url(#barPothole)"
radius={[4, 4, 0, 0]}
isAnimationActive={false}
/>
<Bar
dataKey="signboards"
name="Signboards"
fill="url(#barGradientSignboard)"
dataKey="defected_sign_board"
name="Defected Signboard"
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]}
isAnimationActive={false}
/>

View File

@@ -118,9 +118,26 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
{/* Detection markers */}
{validDetections.map((detection, idx) => {
const isDetPothole = detection.type === "pothole"
const markerColor = isDetPothole ? "#ef4444" : "#3b82f6"
const strokeColor = isDetPothole ? "#991b1b" : "#1e40af"
const type = (detection.type || "").toLowerCase()
let markerColor = "#64748b" // Default Slate
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 (
<CircleMarker
@@ -135,8 +152,8 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
>
<Popup>
<div className="text-xs space-y-1">
<div className="font-semibold">
{isDetPothole ? "Pothole" : (detection.class || detection.type || "").replace(/_/g, " ")} #{detection.id}
<div className="font-semibold capitalize">
{(detection.type || "").replace(/_/g, " ")} #{detection.id}
</div>
<div>Frame: {detection.frame_number}</div>
<div>Confidence: {(detection.confidence * 100).toFixed(1)}%</div>

View File

@@ -1,7 +1,7 @@
"use client"
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"
type SummarySectionProps = {
@@ -10,26 +10,47 @@ type SummarySectionProps = {
export function SummarySection({ data }: SummarySectionProps) {
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",
value: data.summary.unique_potholes || 0,
value: data.summary.unique_pothole || 0,
icon: AlertTriangle,
color: "text-red-500",
bgColor: "bg-red-50 dark:bg-red-950/30",
},
{
label: "Total Detections",
value: data.summary.total_detections || 0,
icon: Target,
color: "text-blue-500",
bgColor: "bg-blue-50 dark:bg-blue-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: "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",
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",
@@ -52,28 +73,35 @@ export function SummarySection({ data }: SummarySectionProps) {
color: "text-blue-500",
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 (
<Card>
<CardHeader>
<CardTitle>Detection Summary</CardTitle>
<CardDescription>Overview of pothole detection results</CardDescription>
<CardDescription>Overview of road analysis results</CardDescription>
</CardHeader>
<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) => {
const Icon = stat.icon
return (
<div
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`}>
<Icon className={`h-5 w-5 ${stat.color}`} />
</div>
<div className={`text-3xl font-bold ${stat.color} mb-1`}>{stat.value}</div>
<div className="text-xs text-muted-foreground text-center">{stat.label}</div>
<div className={`text-xl font-bold ${stat.color} mb-1`}>{stat.value}</div>
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground text-center">{stat.label}</div>
</div>
)
})}

View File

@@ -283,33 +283,47 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
const isSignboard = detectionType === "sign-board-detection" || isCombined
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",
value: data.summary.total_detections || 0,
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: "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",
label: "Unique Potholes",
value: data.summary.unique_pothole || 0,
icon: AlertTriangle,
color: "text-red-500",
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",
@@ -332,6 +346,13 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
color: "text-blue-500",
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 (
@@ -356,7 +377,7 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh
</div>
</CardHeader>
<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) => {
const Icon = stat.icon
return (
@@ -526,12 +547,19 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection
const width = x2 - x1
const height = y2 - y1
// Determine if this specific detection is a pothole or signboard
const isDetPothole = detection._detType === 'pothole' || detection.type === 'pothole' || (detection.pothole_id !== undefined && !detection.signboard_id)
// Map individual detection types to colors
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 = isDetPothole ? '#ef4444' : '#3b82f6' // red for potholes, blue for signboards
const textBgColor = isDetPothole ? 'rgba(239, 68, 68, 0.9)' : 'rgba(59, 130, 246, 0.9)'
const boxColor = detectionColors[type] || '#3b82f6'
const textBgColor = boxColor + 'e6' // Adding alpha
// Draw bounding box
ctx.strokeStyle = boxColor
@@ -539,13 +567,13 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection
ctx.strokeRect(x1, y1, width, height)
// 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)
// Prepare label text
const id = detection.pothole_id ?? detection.signboard_id ?? detection.detection_id
const confidence = (detection.confidence * 100).toFixed(1)
let labelText = isDetPothole
let labelText = type === 'pothole'
? `Pothole #${id}`
: `${(detection.type || 'Sign').replace(/_/g, ' ')} #${id}`
labelText += ` ${confidence}%`

View File

@@ -278,8 +278,12 @@ export interface Video {
filename: string
detection_type: "pothole-detection" | "sign-board-detection" | "pot-sign-detection"
status: "pending" | "processing" | "completed" | "failed"
unique_potholes?: number
unique_signboards?: number
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
created_at: string
updated_at: string
@@ -289,16 +293,35 @@ export interface Video {
* Fetch all videos (for dashboard)
*/
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
return response.videos.map(v => ({
id: v.video_id,
filename: v.video_id, // Using video_id as filename since backend doesn't provide it
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,
filename: v.video_id,
detection_type: "pot-sign-detection" as const,
status: v.status as Video["status"],
unique_potholes: v.summary?.unique_potholes,
unique_signboards: v.summary?.unique_signboards,
unique_defected_sign_board: v.summary?.unique_defected_sign_board,
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,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()

49
lib/project-service.ts Normal file
View 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"

View File

@@ -1,5 +1,18 @@
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 = {
video_id: string
detection_type?: string
@@ -11,34 +24,22 @@ export type DetectionData = {
total_frames: number
}
summary: {
unique_potholes?: number
unique_signboards?: number
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
total_frames: number
detection_rate: number
}
pothole_list?: Array<{
pothole_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
}>
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
}>
pothole_list?: Array<DetectionListItem>
defected_sign_board_list?: Array<DetectionListItem>
road_crack_list?: Array<DetectionListItem>
damaged_road_marking_list?: Array<DetectionListItem>
good_sign_board_list?: Array<DetectionListItem>
signboard_list?: Array<DetectionListItem> // Keeping for backward compatibility
frames: Array<{
frame_id: number
// Legacy format: separate arrays