feat: modify map
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user