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

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>