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" />
/// <reference types="next/image-types/global" /> /// <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 // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

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

View File

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

View File

@@ -23,11 +23,13 @@
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.702 0.183 293.541); --ring: oklch(0.702 0.183 293.541);
--chart-1: oklch(0.811 0.111 293.571); --chart-1: oklch(0.65 0.18 25); /* Soft Red/Rose - High Priority */
--chart-2: oklch(0.606 0.25 292.717); --chart-2: oklch(0.78 0.12 75); /* Warm Amber - Distinct */
--chart-3: oklch(0.541 0.281 293.009); --chart-3: oklch(0.68 0.12 245); /* Azure Blue - Cool Professional */
--chart-4: oklch(0.491 0.27 292.581); --chart-4: oklch(0.75 0.1 165); /* Mint Teal - Balanced */
--chart-5: oklch(0.432 0.232 292.759); --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: oklch(0.985 0 0 / 0.6);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.541 0.281 293.009); --sidebar-primary: oklch(0.541 0.281 293.009);
@@ -57,11 +59,13 @@
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.38 0.189 293.745); --ring: oklch(0.38 0.189 293.745);
--chart-1: oklch(0.811 0.111 293.571); --chart-1: oklch(0.7 0.16 25);
--chart-2: oklch(0.606 0.25 292.717); --chart-2: oklch(0.82 0.14 75);
--chart-3: oklch(0.541 0.281 293.009); --chart-3: oklch(0.72 0.14 245);
--chart-4: oklch(0.491 0.27 292.581); --chart-4: oklch(0.78 0.12 165);
--chart-5: oklch(0.432 0.232 292.759); --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: oklch(0.21 0.006 285.885 / 0.6);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.606 0.25 292.717); --sidebar-primary: oklch(0.606 0.25 292.717);
@@ -100,6 +104,8 @@
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5); --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-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);

View File

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

View File

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

View File

@@ -3,7 +3,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 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 } from '@/types';
import { DETECTION_TYPES } from '@/constants/detectionModeConfig'; import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
@@ -35,6 +36,8 @@ export function DashboardMap({
const [detections, setDetections] = useState<Detection[]>([]); const [detections, setDetections] = useState<Detection[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isMaximized, setIsMaximized] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
if (!projectSummary) { if (!projectSummary) {
@@ -62,8 +65,10 @@ export function DashboardMap({
for (const [chnName, chn] of Object.entries(chainagesToProcess)) { for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
if (!chn) continue; if (!chn) continue;
const chainageDetections = (chn as any).detections || []; const chainageDetections = ((chn as any).detections || []).filter(
filteredDetections.push(...chainageDetections); (d: any) => (d.type || d.class || '').toLowerCase() !== 'good_sign_board'
);
filteredDetections.push(...chainageDetections);
} }
} }
@@ -76,50 +81,93 @@ export function DashboardMap({
} }
}, [projectSummary, selectedPackageId, selectedChainageId]); }, [projectSummary, selectedPackageId, selectedChainageId]);
// Filter detections with valid GPS coordinates // Handle detection type toggle
const validDetections = detections.filter((d) => d.latitude && d.longitude); 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 ( return (
<Card className={`overflow-hidden ${className}`}> <Card
<CardHeader className="pb-2"> className={`overflow-hidden transition-all duration-300 p-2 rounded-lg ${
<div className="flex items-center justify-between"> isMaximized ? 'fixed inset-0 z-100 m-0 rounded-none bg-background' : className
<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" /> <CardContent className="p-0 relative rounded-lg">
</div> {/* Top Right Actions */}
<div> <div className="absolute top-4 right-4 z-1000 pointer-events-none">
<CardTitle className="text-base font-bold">Detection Map</CardTitle> <div className="pointer-events-auto">
<p className="text-xs text-muted-foreground"> <Button
{isLoading variant="default"
? 'Loading...' size="icon"
: `${validDetections.length} detections with GPS coordinates`} onClick={() => setIsMaximized(!isMaximized)}
</p> title={isMaximized ? "Exit Fullscreen" : "Maximize Map"}
</div> >
</div> {isMaximized ? <Minimize2 className="h-5 w-5" /> : <Maximize2 className="h-5 w-5" />}
</Button>
{/* 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>
);
})}
</div> </div>
</div> </div>
</CardHeader>
<CardContent className="p-0"> {/* Bottom Left Legend */}
<div className="h-[500px] p-2 w-full relative"> <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 ? ( {isLoading ? (
<div className="h-full w-full flex items-center justify-center bg-muted/50"> <div className="h-full w-full flex items-center justify-center bg-muted/50">
<Loader2 className="h-8 w-8 text-primary animate-spin" /> <Loader2 className="h-8 w-8 text-primary animate-spin" />
@@ -140,6 +188,9 @@ export function DashboardMap({
) : ( ) : (
<DashboardMapContent detections={validDetections} /> <DashboardMapContent detections={validDetections} />
)} )}
{/* Bottom Left Label Overlay */}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

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

View File

@@ -119,20 +119,34 @@ export default function MapModal({ open, onClose, detections, detectionType }: M
fillOpacity={0.9} fillOpacity={0.9}
> >
<Popup> <Popup>
<div className="text-[11px] space-y-2 p-1"> <div className="text-sm min-w-[200px] py-1">
<div className="font-bold border-b pb-1 capitalize"> <div className="font-bold text-base border-b border-border pb-2 mb-3 leading-none capitalize">
{(detection.type || '').replace(/_/g, ' ')} #{detection.id} {(detection.type || '').replace(/_/g, ' ')} <span className="text-muted-foreground font-medium text-sm ml-1">#{detection.id}</span>
</div> </div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
<span className="text-muted-foreground">Frame:</span> <div className="space-y-2 mb-4">
<span className="font-semibold text-right">{detection.frame_number}</span> <div className="flex justify-between items-baseline gap-4">
<span className="text-muted-foreground">Confidence:</span> <span className="text-muted-foreground text-xs">Frame</span>
<span className="font-semibold text-right"> <span className="font-medium text-right font-mono">{detection.frame_number}</span>
{(detection.confidence * 100).toFixed(0)}% </div>
</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>
<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>
</div> </div>
</Popup> </Popup>

View File

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