217 lines
7.2 KiB
TypeScript
217 lines
7.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Loader2, Milestone, Maximize2, Minimize2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Detection } from '@/types';
|
|
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
|
|
|
|
// Dynamically import the map to avoid SSR issues with Leaflet
|
|
const DashboardMapContent = dynamic(() => import('./dashboard-map-content'), {
|
|
ssr: false,
|
|
loading: () => (
|
|
<div className="h-full w-full flex items-center justify-center">
|
|
<Loader2 className="h-8 w-8 text-primary" />
|
|
</div>
|
|
),
|
|
});
|
|
|
|
interface DashboardMapProps {
|
|
className?: string;
|
|
selectedProjectId?: string | null;
|
|
selectedPackageId?: string | null;
|
|
selectedChainageId?: string | null;
|
|
projectSummary?: any;
|
|
}
|
|
|
|
export function DashboardMap({
|
|
className,
|
|
selectedProjectId,
|
|
selectedPackageId,
|
|
selectedChainageId,
|
|
projectSummary,
|
|
}: DashboardMapProps) {
|
|
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) {
|
|
setDetections([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
const filteredDetections: Detection[] = [];
|
|
|
|
const packagesToProcess =
|
|
selectedPackageId && selectedPackageId !== 'all'
|
|
? { [selectedPackageId]: projectSummary.packages[selectedPackageId] }
|
|
: projectSummary.packages || {};
|
|
|
|
for (const [pkgName, pkg] of Object.entries(packagesToProcess)) {
|
|
if (!pkg) continue;
|
|
|
|
const chainagesToProcess =
|
|
selectedChainageId && selectedChainageId !== 'all'
|
|
? {
|
|
[selectedChainageId]: (pkg as any).chainages?.[
|
|
selectedChainageId
|
|
],
|
|
}
|
|
: (pkg as any).chainages || {};
|
|
|
|
for (const [chnName, chn] of Object.entries(chainagesToProcess)) {
|
|
if (!chn) continue;
|
|
const chainageDetections = ((chn as any).detections || []).filter(
|
|
(d: any) =>
|
|
(d.type || d.class || '').toLowerCase() !== 'good_sign_board',
|
|
);
|
|
filteredDetections.push(...chainageDetections);
|
|
}
|
|
}
|
|
|
|
setDetections(filteredDetections);
|
|
} catch (err) {
|
|
console.error('Failed to extract detections:', err);
|
|
setError('Failed to load detection data');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [projectSummary, selectedPackageId, selectedChainageId]);
|
|
|
|
// 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 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>
|
|
|
|
{/* 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" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="h-full w-full flex items-center justify-center bg-muted/50">
|
|
<p className="text-muted-foreground">{error}</p>
|
|
</div>
|
|
) : validDetections.length === 0 ? (
|
|
<div className="h-full w-full flex items-center justify-center bg-muted/50">
|
|
<div className="text-center">
|
|
<p className="text-muted-foreground">
|
|
No detections with GPS coordinates found
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mt-1 opacity-70">
|
|
Process some videos to see detections on the map
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<DashboardMapContent detections={validDetections} />
|
|
)}
|
|
|
|
{/* Bottom Left Label Overlay */}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|