feat: modify map

This commit is contained in:
2026-03-19 18:07:37 +05:30
parent 32b2c3c6ab
commit 02c5425fce
10 changed files with 341 additions and 158 deletions

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -20,6 +20,7 @@ import {
PencilLine,
CheckCircle2,
Settings2,
Droplets,
} from 'lucide-react';
import { StatsCard } from '@/components/dashboard/stats-card';
import { FilterSelector } from '@/components/dashboard/filter-selector';
@@ -74,6 +75,8 @@ interface DetectionStats {
totalRoadCrack: number;
totalDamagedRoadMarking: number;
totalGoodSignboard: number;
totalDrainIssue: number;
totalDefectiveCulvert: number;
totalRoadDamage: number;
chainageData: Array<{
name: string;
@@ -81,7 +84,9 @@ interface DetectionStats {
pothole: number;
road_crack: number;
damaged_road_marking: number;
good_sign_board: number;
// good_sign_board: number;
drain_issue: number;
defective_culvert: number;
total: number;
}>;
}
@@ -94,6 +99,8 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
totalRoadCrack: 0,
totalDamagedRoadMarking: 0,
totalGoodSignboard: 0,
totalDrainIssue: 0,
totalDefectiveCulvert: 0,
totalRoadDamage: 0,
chainageData: [],
};
@@ -104,21 +111,23 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
let total_road_crack = 0;
let total_damaged_road_marking = 0;
let total_good_sign_board = 0;
let total_drain_issue = 0;
let total_defective_culvert = 0;
let total_road_damage = 0;
const chainageData: DetectionStats['chainageData'] = [];
for (const pkg of Object.values(summary.packages || {})) {
for (const [chnName, chn] of Object.entries(pkg.chainages || {})) {
// Changed 'locName, loc' to 'chnName, chn' and 'pkg.locations' to 'pkg.chainages'
let chnDefectedSignboard = 0; // Changed 'locDefectedSignboard' to 'chnDefectedSignboard'
let chnPothole = 0; // Changed 'locPothole' to 'chnPothole'
let chnRoadCrack = 0; // Changed 'locRoadCrack' to 'chnRoadCrack'
let chnDamagedRoadMarking = 0; // Changed 'locDamagedRoadMarking' to 'chnDamagedRoadMarking'
let chnGoodSignboard = 0; // Changed 'locGoodSignboard' to 'chnGoodSignboard'
let chnDefectedSignboard = 0;
let chnPothole = 0;
let chnRoadCrack = 0;
let chnDamagedRoadMarking = 0;
let chnGoodSignboard = 0;
let chnDrainIssue = 0;
let chnDefectiveCulvert = 0;
for (const detection of chn.detections || []) {
// Changed 'loc.detections' to 'chn.detections'
const type = detection.type.toLowerCase();
const type = (detection.type || '').toLowerCase();
if (type === 'defected_sign_board') {
chnDefectedSignboard++;
total_defected_sign_board++;
@@ -133,42 +142,55 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
total_damaged_road_marking++;
} else if (type === 'good_sign_board') {
chnGoodSignboard++;
total_good_sign_board++;
// total_good_sign_board++;
} else if (type === 'drain_issue') {
chnDrainIssue++;
total_drain_issue++;
} else if (type === 'defective_culvert') {
chnDefectiveCulvert++;
total_defective_culvert++;
}
}
const chnTotalDamage = // Changed 'locTotalDamage' to 'chnTotalDamage'
chnDefectedSignboard + chnPothole + chnRoadCrack + chnDamagedRoadMarking;
if (
chnDefectedSignboard > 0 ||
chnPothole > 0 ||
chnRoadCrack > 0 ||
chnDamagedRoadMarking > 0 ||
chnGoodSignboard > 0
chnGoodSignboard > 0 ||
chnDrainIssue > 0 ||
chnDefectiveCulvert > 0
) {
const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName;
chainageData.push({
// Changed 'locationData.push' to 'chainageData.push'
name: shortName,
defected_sign_board: chnDefectedSignboard,
pothole: chnPothole,
road_crack: chnRoadCrack,
damaged_road_marking: chnDamagedRoadMarking,
good_sign_board: chnGoodSignboard,
// good_sign_board: chnGoodSignboard,
drain_issue: chnDrainIssue,
defective_culvert: chnDefectiveCulvert,
total:
chnDefectedSignboard +
chnPothole +
chnRoadCrack +
chnDamagedRoadMarking +
chnGoodSignboard,
// chnGoodSignboard +
chnDrainIssue +
chnDefectiveCulvert,
});
}
}
}
total_road_damage =
total_defected_sign_board + total_pothole + total_road_crack + total_damaged_road_marking;
total_defected_sign_board +
total_pothole +
total_road_crack +
total_damaged_road_marking +
total_drain_issue +
total_defective_culvert;
return {
totalDefectedSignboard: total_defected_sign_board,
@@ -176,6 +198,8 @@ function calculateStats(summary: ProjectSummary | null): DetectionStats {
totalRoadCrack: total_road_crack,
totalDamagedRoadMarking: total_damaged_road_marking,
totalGoodSignboard: total_good_sign_board,
totalDrainIssue: total_drain_issue,
totalDefectiveCulvert: total_defective_culvert,
totalRoadDamage: total_road_damage,
chainageData,
};
@@ -192,6 +216,8 @@ export default function DashboardPage() {
totalRoadCrack: 0,
totalDamagedRoadMarking: 0,
totalGoodSignboard: 0,
totalDrainIssue: 0,
totalDefectiveCulvert: 0,
totalRoadDamage: 0,
chainageData: [],
});
@@ -291,6 +317,8 @@ export default function DashboardPage() {
totalRoadCrack: 0,
totalDamagedRoadMarking: 0,
totalGoodSignboard: 0,
totalDrainIssue: 0,
totalDefectiveCulvert: 0,
totalRoadDamage: 0,
chainageData: [], // Changed 'locationData' to 'chainageData'
});
@@ -302,6 +330,8 @@ export default function DashboardPage() {
let totalRoadCrack = 0;
let totalDamagedRoadMarking = 0;
let totalGoodSignboard = 0;
let totalDrainIssue = 0;
let totalDefectiveCulvert = 0;
const chainageData: DetectionStats['chainageData'] = [];
const packagesToProcess =
@@ -323,9 +353,11 @@ export default function DashboardPage() {
let chnRoadCrack = 0;
let chnDamagedRoadMarking = 0;
let chnGoodSignboard = 0;
let chnDrainIssue = 0;
let chnDefectiveCulvert = 0;
for (const detection of chn.detections || []) {
const type = detection.type.toLowerCase();
const type = (detection.type || '').toLowerCase();
if (type === 'defected_sign_board') {
chnDefectedSignboard++;
totalDefectedSignboard++;
@@ -339,8 +371,14 @@ export default function DashboardPage() {
chnDamagedRoadMarking++;
totalDamagedRoadMarking++;
} else if (type === 'good_sign_board') {
chnGoodSignboard++;
totalGoodSignboard++;
// chnGoodSignboard++;
// totalGoodSignboard++;
} else if (type === 'drain_issue') {
chnDrainIssue++;
totalDrainIssue++;
} else if (type === 'defective_culvert') {
chnDefectiveCulvert++;
totalDefectiveCulvert++;
}
}
@@ -349,7 +387,9 @@ export default function DashboardPage() {
chnPothole > 0 ||
chnRoadCrack > 0 ||
chnDamagedRoadMarking > 0 ||
chnGoodSignboard > 0
chnGoodSignboard > 0 ||
chnDrainIssue > 0 ||
chnDefectiveCulvert > 0
) {
const shortName = chnName.length > 20 ? chnName.substring(0, 20) + '...' : chnName;
chainageData.push({
@@ -358,13 +398,17 @@ export default function DashboardPage() {
pothole: chnPothole,
road_crack: chnRoadCrack,
damaged_road_marking: chnDamagedRoadMarking,
good_sign_board: chnGoodSignboard,
// good_sign_board: chnGoodSignboard,
drain_issue: chnDrainIssue,
defective_culvert: chnDefectiveCulvert,
total:
chnDefectedSignboard +
chnPothole +
chnRoadCrack +
chnDamagedRoadMarking +
chnGoodSignboard,
// chnGoodSignboard +
chnDrainIssue +
chnDefectiveCulvert,
});
}
}
@@ -376,8 +420,15 @@ export default function DashboardPage() {
totalRoadCrack,
totalDamagedRoadMarking,
totalGoodSignboard,
totalDrainIssue,
totalDefectiveCulvert,
totalRoadDamage:
totalDefectedSignboard + totalPothole + totalRoadCrack + totalDamagedRoadMarking,
totalDefectedSignboard +
totalPothole +
totalRoadCrack +
totalDamagedRoadMarking +
totalDrainIssue +
totalDefectiveCulvert,
chainageData,
});
}, [projectSummary, selectedPackageId, selectedChainageId]);
@@ -479,13 +530,21 @@ export default function DashboardPage() {
/>
</StaggerItem>
<StaggerItem>
<StatsCard
title="Drain Issues"
subtitle="Clogged or broken drainage"
value={stats.totalDrainIssue}
icon={Droplets}
/>
</StaggerItem>
{/* <StaggerItem>
<StatsCard
title="Good Signboards"
subtitle="Informational markers"
value={stats.totalGoodSignboard}
icon={CheckCircle2}
/>
</StaggerItem>
</StaggerItem> */}
</StaggerContainer>
{/* Charts Row - Side by Side */}
@@ -502,12 +561,18 @@ export default function DashboardPage() {
pothole={stats.totalPothole}
roadCrack={stats.totalRoadCrack}
damagedRoadMarking={stats.totalDamagedRoadMarking}
goodSignboard={stats.totalGoodSignboard}
// goodSignboard={stats.totalGoodSignboard}
drainIssue={stats.totalDrainIssue}
defectiveCulvert={stats.totalDefectiveCulvert}
/>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
Total detections:{' '}
{stats.totalRoadDamage +
// stats.totalGoodSignboard +
stats.totalDrainIssue +
stats.totalDefectiveCulvert}
</div>
</CardFooter>
</Card>

View File

@@ -30,6 +30,7 @@ const DETECTION_METHODS = [
{ value: 'yoloe', label: 'YOLOE Open-Vocabulary Detection' },
{ value: 'yoloe_trained_vl', label: 'YOLOE With Vision Language Model' },
{ value: 'culvert_detection', label: 'Culvert Detection' },
{ value: 'combined', label: 'Combined Detection Model' },
] as const;
export default function UploadPage() {

View File

@@ -23,11 +23,13 @@
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.702 0.183 293.541);
--chart-1: oklch(0.811 0.111 293.571);
--chart-2: oklch(0.606 0.25 292.717);
--chart-3: oklch(0.541 0.281 293.009);
--chart-4: oklch(0.491 0.27 292.581);
--chart-5: oklch(0.432 0.232 292.759);
--chart-1: oklch(0.65 0.18 25); /* Soft Red/Rose - High Priority */
--chart-2: oklch(0.78 0.12 75); /* Warm Amber - Distinct */
--chart-3: oklch(0.68 0.12 245); /* Azure Blue - Cool Professional */
--chart-4: oklch(0.75 0.1 165); /* Mint Teal - Balanced */
--chart-5: oklch(0.85 0.08 195); /* Soft Cyan - Subdued */
--chart-6: oklch(0.62 0.22 295); /* Deep Violet - Theme Primary */
--chart-8: oklch(0.58 0.2 335); /* Cool Magenta - Distant Accent */
--sidebar: oklch(0.985 0 0 / 0.6);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.541 0.281 293.009);
@@ -57,11 +59,13 @@
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.38 0.189 293.745);
--chart-1: oklch(0.811 0.111 293.571);
--chart-2: oklch(0.606 0.25 292.717);
--chart-3: oklch(0.541 0.281 293.009);
--chart-4: oklch(0.491 0.27 292.581);
--chart-5: oklch(0.432 0.232 292.759);
--chart-1: oklch(0.7 0.16 25);
--chart-2: oklch(0.82 0.14 75);
--chart-3: oklch(0.72 0.14 245);
--chart-4: oklch(0.78 0.12 165);
--chart-5: oklch(0.88 0.1 195);
--chart-6: oklch(0.65 0.24 295);
--chart-8: oklch(0.62 0.22 335);
--sidebar: oklch(0.21 0.006 285.885 / 0.6);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.606 0.25 292.717);
@@ -100,6 +104,8 @@
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-chart-6: var(--chart-6);
--color-chart-8: var(--chart-8);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);

View File

@@ -10,6 +10,7 @@ import {
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface ChainageData {
name: string;
@@ -17,6 +18,8 @@ interface ChainageData {
pothole: number;
road_crack: number;
damaged_road_marking: number;
drain_issue: number;
defective_culvert: number;
total: number;
}
@@ -27,20 +30,28 @@ interface ChainageBarChartProps {
const chartConfig = {
pothole: {
label: 'Potholes',
color: 'var(--chart-1)',
label: DETECTION_TYPES.pothole.label + 's',
color: DETECTION_TYPES.pothole.color,
},
defected_sign_board: {
label: 'Defected Signboards',
color: 'var(--chart-2)',
label: DETECTION_TYPES.defected_sign_board.label + 's',
color: DETECTION_TYPES.defected_sign_board.color,
},
road_crack: {
label: 'Road Cracks',
color: 'var(--chart-3)',
label: DETECTION_TYPES.road_crack.label + 's',
color: DETECTION_TYPES.road_crack.color,
},
damaged_road_marking: {
label: 'Damaged Markings',
color: 'var(--chart-4)',
label: DETECTION_TYPES.damaged_road_marking.label + 's',
color: DETECTION_TYPES.damaged_road_marking.color,
},
drain_issue: {
label: DETECTION_TYPES.drain_issue.label + 's',
color: DETECTION_TYPES.drain_issue.color,
},
defective_culvert: {
label: DETECTION_TYPES.defective_culvert.label + 's',
color: DETECTION_TYPES.defective_culvert.color,
},
} satisfies ChartConfig;
@@ -69,6 +80,8 @@ export function ChainageBarChart({ data, isLoading = false }: ChainageBarChartPr
defected_sign_board: item.defected_sign_board,
road_crack: item.road_crack,
damaged_road_marking: item.damaged_road_marking,
drain_issue: item.drain_issue,
defective_culvert: item.defective_culvert,
}));
return (
@@ -90,6 +103,8 @@ export function ChainageBarChart({ data, isLoading = false }: ChainageBarChartPr
<Bar dataKey="defected_sign_board" fill="var(--color-defected_sign_board)" radius={4} />
<Bar dataKey="road_crack" fill="var(--color-road_crack)" radius={4} />
<Bar dataKey="damaged_road_marking" fill="var(--color-damaged_road_marking)" radius={4} />
<Bar dataKey="drain_issue" fill="var(--color-drain_issue)" radius={4} />
<Bar dataKey="defective_culvert" fill="var(--color-defective_culvert)" radius={4} />
</BarChart>
</ChartContainer>
</div>

View File

@@ -5,6 +5,7 @@ import { MapContainer, TileLayer, CircleMarker, Popup, useMap } from 'react-leaf
import { LatLngBounds, LatLng } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { Detection } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface DashboardMapContentProps {
detections: Detection[];
@@ -52,28 +53,21 @@ export default function DashboardMapContent({ detections }: DashboardMapContentP
];
// Get marker color based on detection type
const getMarkerColor = (type: string) => {
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
const getMarkerColor = (typeId: string) => {
const config = DETECTION_TYPES[typeId.toLowerCase()];
if (config) {
return {
fill: config.color,
stroke: '#ffffff' // White stroke for better visibility on the map
};
}
return { fill: '#64748b', stroke: '#475569' }; // Default Slate
};
// Get display name for detection type
const getTypeName = (type: string) => {
return type
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
const getTypeName = (typeId: string) => {
const config = DETECTION_TYPES[typeId.toLowerCase()];
return config ? config.label : typeId.replace(/_/g, ' ');
};
return (
@@ -106,23 +100,33 @@ export default function DashboardMapContent({ detections }: DashboardMapContentP
fillOpacity={0.8}
>
<Popup>
<div className="text-sm space-y-2 min-w-[180px]">
<div className="font-bold text-base border-b pb-1">{typeName}</div>
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-muted-foreground">Class:</span>
<span className="font-medium">{detection.class.replace(/_/g, ' ')}</span>
<div className="text-sm min-w-[200px] py-1">
<div className="font-bold text-base border-b border-border pb-2 mb-3 leading-none">
{typeName}
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">Class</span>
<span className="font-medium text-right capitalize">{detection.class.replace(/_/g, ' ')}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Confidence:</span>
<span className="font-medium">{(detection.confidence * 100).toFixed(1)}%</span>
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">Confidence</span>
<span className="font-medium text-right">{(detection.confidence * 100).toFixed(1)}%</span>
</div>
</div>
<div className="pt-2 border-t">
<div className="text-xs text-muted-foreground mb-1">Coordinates</div>
<div className="font-mono text-xs bg-muted/30 p-2 rounded">
<div>Lat: {detection.latitude!.toFixed(6)}</div>
<div>Lng: {detection.longitude!.toFixed(6)}</div>
<div className="pt-2 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground mb-2">Location Details</div>
<div className="grid grid-cols-2 gap-3 bg-muted/40 p-2.5 rounded-md font-mono text-[11px]">
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">Latitude</span>
<span className="font-medium tracking-tighter">{detection.latitude!.toFixed(6)}</span>
</div>
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">Longitude</span>
<span className="font-medium tracking-tighter">{detection.longitude!.toFixed(6)}</span>
</div>
</div>
</div>
</div>

View File

@@ -3,7 +3,8 @@
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2, Milestone } from 'lucide-react';
import { Loader2, Milestone, Maximize2, Minimize2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Detection } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
@@ -35,6 +36,8 @@ export function DashboardMap({
const [detections, setDetections] = useState<Detection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isMaximized, setIsMaximized] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
useEffect(() => {
if (!projectSummary) {
@@ -62,8 +65,10 @@ export function DashboardMap({
for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
if (!chn) continue;
const chainageDetections = (chn as any).detections || [];
filteredDetections.push(...chainageDetections);
const chainageDetections = ((chn as any).detections || []).filter(
(d: any) => (d.type || d.class || '').toLowerCase() !== 'good_sign_board'
);
filteredDetections.push(...chainageDetections);
}
}
@@ -76,50 +81,93 @@ export function DashboardMap({
}
}, [projectSummary, selectedPackageId, selectedChainageId]);
// Filter detections with valid GPS coordinates
const validDetections = detections.filter((d) => d.latitude && d.longitude);
// Handle detection type toggle
const toggleType = (typeId: string) => {
setSelectedTypes((prev) => {
if (prev.includes(typeId)) {
const next = prev.filter((id) => id !== typeId);
return next.length === 0 ? [] : next;
} else {
return [...prev, typeId];
}
});
};
// Filter detections with valid GPS coordinates and selected types
const validDetections = detections.filter((d) => {
const hasGps = d.latitude && d.longitude;
if (!hasGps) return false;
if (selectedTypes.length === 0) return true;
const typeId = (d.type || d.class || '').toLowerCase();
return selectedTypes.includes(typeId);
});
return (
<Card className={`overflow-hidden ${className}`}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-secondary text-secondary-foreground flex items-center justify-center">
<Milestone className="h-5 w-5" />
</div>
<div>
<CardTitle className="text-base font-bold">Detection Map</CardTitle>
<p className="text-xs text-muted-foreground">
{isLoading
? 'Loading...'
: `${validDetections.length} detections with GPS coordinates`}
</p>
</div>
</div>
{/* Dynamic Color Legend */}
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1 text-[10px] max-w-[60%]">
{Object.values(DETECTION_TYPES).map((type) => {
const count = validDetections.filter(
(d) => (d.type || d.class || '').toLowerCase() === type.id.toLowerCase()
).length;
if (count === 0 && (type.id.includes('culvert') || type.id === 'drain_issue')) return null;
return (
<div key={type.id} className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: type.color }} />
<span className="text-muted-foreground font-medium">
{type.label} ({count})
</span>
</div>
);
})}
<Card
className={`overflow-hidden transition-all duration-300 p-2 rounded-lg ${
isMaximized ? 'fixed inset-0 z-100 m-0 rounded-none bg-background' : className
}`}
>
<CardContent className="p-0 relative rounded-lg">
{/* Top Right Actions */}
<div className="absolute top-4 right-4 z-1000 pointer-events-none">
<div className="pointer-events-auto">
<Button
variant="default"
size="icon"
onClick={() => setIsMaximized(!isMaximized)}
title={isMaximized ? "Exit Fullscreen" : "Maximize Map"}
>
{isMaximized ? <Minimize2 className="h-5 w-5" /> : <Maximize2 className="h-5 w-5" />}
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="h-[500px] p-2 w-full relative">
{/* Bottom Left Legend */}
<div className="absolute bottom-4 left-4 z-1000 pointer-events-none">
<div className="pointer-events-auto bg-zinc-950/80 backdrop-blur-xl border rounded-md p-2">
<div className="flex flex-wrap items-center gap-2 max-w-4xl">
{Object.values(DETECTION_TYPES).map((type) => {
const typeId = type.id.toLowerCase();
const count = detections.filter(
(d) => (d.type || d.class || '').toLowerCase() === typeId && d.latitude && d.longitude
).length;
if (
count === 0 &&
(type.id.includes('culvert') || type.id === 'drain_issue')
)
return null;
if (type.id === 'good_sign_board') return null;
const isSelected = selectedTypes.length === 0 || selectedTypes.includes(typeId);
return (
<button
key={type.id}
onClick={() => toggleType(typeId)}
className={`flex items-center gap-2 px-2 py-1.5 text-zinc-100 rounded-full transition-all text-xs ${
isSelected
? 'bg-zinc-800/80 '
: 'bg-transparent '
}`}
>
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: type.color }}
/>
<span>
{type.label} ({count})
</span>
</button>
);
})}
</div>
</div>
</div>
<div className={`${isMaximized ? 'h-screen' : 'h-[600px]'} w-full relative transition-all`}>
{isLoading ? (
<div className="h-full w-full flex items-center justify-center bg-muted/50">
<Loader2 className="h-8 w-8 text-primary animate-spin" />
@@ -140,6 +188,9 @@ export function DashboardMap({
) : (
<DashboardMapContent detections={validDetections} />
)}
{/* Bottom Left Label Overlay */}
</div>
</CardContent>
</Card>

View File

@@ -10,36 +10,43 @@ import {
ChartTooltipContent,
type ChartConfig,
} from '@/components/ui/chart';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
interface DetectionDonutChartProps {
defectedSignboard: number;
pothole: number;
roadCrack: number;
damagedRoadMarking: number;
goodSignboard: number;
// goodSignboard: number;
drainIssue: number;
defectiveCulvert: number;
isLoading?: boolean;
}
const chartConfig = {
pothole: {
label: 'Potholes',
color: 'var(--chart-1)',
label: DETECTION_TYPES.pothole.label + 's',
color: DETECTION_TYPES.pothole.color,
},
defectedSignboard: {
label: 'Defected Signboards',
color: 'var(--chart-2)',
label: DETECTION_TYPES.defected_sign_board.label + 's',
color: DETECTION_TYPES.defected_sign_board.color,
},
roadCrack: {
label: 'Road Cracks',
color: 'var(--chart-3)',
label: DETECTION_TYPES.road_crack.label + 's',
color: DETECTION_TYPES.road_crack.color,
},
damagedRoadMarking: {
label: 'Damaged Markings',
color: 'var(--chart-4)',
label: DETECTION_TYPES.damaged_road_marking.label + 's',
color: DETECTION_TYPES.damaged_road_marking.color,
},
goodSignboard: {
label: 'Good Signboards',
color: 'var(--chart-5)',
drainIssue: {
label: DETECTION_TYPES.drain_issue.label + 's',
color: DETECTION_TYPES.drain_issue.color,
},
defectiveCulvert: {
label: DETECTION_TYPES.defective_culvert.label + 's',
color: DETECTION_TYPES.defective_culvert.color,
},
} satisfies ChartConfig;
@@ -48,7 +55,9 @@ export function DetectionDonutChart({
pothole,
roadCrack,
damagedRoadMarking,
goodSignboard,
// goodSignboard,
drainIssue,
defectiveCulvert,
isLoading = false,
}: DetectionDonutChartProps) {
const chartData = React.useMemo(
@@ -66,13 +75,31 @@ export function DetectionDonutChart({
count: damagedRoadMarking,
fill: 'var(--color-damagedRoadMarking)',
},
/* {
type: 'goodSignboard',
count: goodSignboard,
fill: 'var(--color-goodSignboard)',
}, */
{
type: 'goodSignboard',
count: goodSignboard,
fill: 'var(--color-goodSignboard)',
type: 'drainIssue',
count: drainIssue,
fill: 'var(--color-drainIssue)',
},
{
type: 'defectiveCulvert',
count: defectiveCulvert,
fill: 'var(--color-defectiveCulvert)',
},
].filter((item) => item.count > 0),
[pothole, defectedSignboard, roadCrack, damagedRoadMarking, goodSignboard],
[
pothole,
defectedSignboard,
roadCrack,
damagedRoadMarking,
// goodSignboard,
drainIssue,
defectiveCulvert,
],
);
const totalDetections = React.useMemo(() => {

View File

@@ -119,20 +119,34 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
fillOpacity={0.9}
>
<Popup>
<div className="text-[11px] space-y-2 p-1">
<div className="font-bold border-b pb-1 capitalize">
{(detection.type || '').replace(/_/g, ' ')} #{detection.id}
<div className="text-sm min-w-[200px] py-1">
<div className="font-bold text-base border-b border-border pb-2 mb-3 leading-none capitalize">
{(detection.type || '').replace(/_/g, ' ')} <span className="text-muted-foreground font-medium text-sm ml-1">#{detection.id}</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
<span className="text-muted-foreground">Frame:</span>
<span className="font-semibold text-right">{detection.frame_number}</span>
<span className="text-muted-foreground">Confidence:</span>
<span className="font-semibold text-right">
{(detection.confidence * 100).toFixed(0)}%
</span>
<div className="space-y-2 mb-4">
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">Frame</span>
<span className="font-medium text-right font-mono">{detection.frame_number}</span>
</div>
<div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground text-xs">Confidence</span>
<span className="font-medium text-right">{(detection.confidence * 100).toFixed(1)}%</span>
</div>
</div>
<div className="font-mono bg-muted p-1.5 rounded border text-center text-[9px]">
{detection.latitude.toFixed(6)}, {detection.longitude.toFixed(6)}
<div className="pt-2 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground mb-2">Location Details</div>
<div className="grid grid-cols-2 gap-3 bg-muted/40 p-2.5 rounded-md font-mono text-[11px]">
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">Latitude</span>
<span className="font-medium tracking-tighter">{detection.latitude.toFixed(6)}</span>
</div>
<div className="space-y-0.5">
<span className="text-[9px] block text-muted-foreground/70 uppercase">Longitude</span>
<span className="font-medium tracking-tighter">{detection.longitude.toFixed(6)}</span>
</div>
</div>
</div>
</div>
</Popup>

View File

@@ -21,7 +21,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
pothole: {
id: 'pothole',
label: 'Pothole',
color: '#ef4444',
color: 'var(--chart-1)',
countKey: 'unique_pothole',
listKey: 'pothole_list',
frameCountKey: 'pothole',
@@ -29,7 +29,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
defected_sign_board: {
id: 'defected_sign_board',
label: 'Defect Sign Board',
color: '#3b82f6',
color: 'var(--chart-2)',
countKey: 'unique_defected_sign_board',
listKey: 'defected_sign_board_list',
frameCountKey: 'defected_sign_board',
@@ -37,7 +37,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
road_crack: {
id: 'road_crack',
label: 'Road Crack',
color: '#f59e0b',
color: 'var(--chart-3)',
countKey: 'unique_road_crack',
listKey: 'road_crack_list',
frameCountKey: 'road_crack',
@@ -45,7 +45,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
damaged_road_marking: {
id: 'damaged_road_marking',
label: 'Damage Road Mark',
color: '#6366f1',
color: 'var(--chart-4)',
countKey: 'unique_damaged_road_marking',
listKey: 'damaged_road_marking_list',
frameCountKey: 'damaged_road_marking',
@@ -53,7 +53,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
good_sign_board: {
id: 'good_sign_board',
label: 'Good Sign Board',
color: '#10b981',
color: 'var(--chart-5)',
countKey: 'unique_good_sign_board',
listKey: 'good_sign_board_list',
frameCountKey: 'good_sign_board',
@@ -61,7 +61,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
drain_issue: {
id: 'drain_issue',
label: 'Drain Issue',
color: '#a855f7',
color: 'var(--chart-6)',
countKey: 'unique_drain_issue',
listKey: 'drain_issue_list',
frameCountKey: 'drain_issue',
@@ -69,7 +69,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
good_culvert: {
id: 'good_culvert',
label: 'Good Culvert',
color: '#10b981',
color: 'var(--chart-5)',
countKey: 'unique_good_culvert',
listKey: 'good_culvert_list',
frameCountKey: 'good_culvert',
@@ -77,7 +77,7 @@ export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
defective_culvert: {
id: 'defective_culvert',
label: 'Defective Culvert',
color: '#ef4444',
color: 'var(--chart-8)',
countKey: 'unique_defective_culvert',
listKey: 'defective_culvert_list',
frameCountKey: 'defective_culvert',