'use client'; import { useEffect, useMemo, useState } from 'react'; import dynamic from 'next/dynamic'; import { AlertTriangle, MapPinned, RefreshCw } from 'lucide-react'; import { getDefectVisual } from '@/constants/defectVisualConfig'; import type { DetectionCoordinateClass, DetectionCoordinateItem, DetectionCoordinatesResponse, } from '@/types'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import type { AssignmentRangePoint } from './assignmentRange'; import { AssignmentRangeActions } from './AssignmentRangeActions'; type AssignmentIssuesMapProps = { data?: DetectionCoordinatesResponse; isLoading: boolean; isFetchingMore: boolean; isError: boolean; hasMore: boolean; startPoint: AssignmentRangePoint | null; endPoint: AssignmentRangePoint | null; onSetStart: (point: AssignmentRangePoint) => void; onSetEnd: (point: AssignmentRangePoint) => void; onLoadMore: () => void; onRetry: () => void; }; type ClientAssignmentIssuesMapProps = { items: DetectionCoordinateItem[]; classes: DetectionCoordinateClass[]; selectedDetectionId: number | null; startPoint: AssignmentRangePoint | null; endPoint: AssignmentRangePoint | null; onSelectDetection: (detectionId: number) => void; onSetStart: (point: AssignmentRangePoint) => void; onSetEnd: (point: AssignmentRangePoint) => void; }; function isValidCoordinate(item: DetectionCoordinateItem) { return ( Number.isFinite(item.latitude) && Number.isFinite(item.longitude) && item.latitude >= -90 && item.latitude <= 90 && item.longitude >= -180 && item.longitude <= 180 ); } const ClientAssignmentIssuesMap = dynamic( async () => { const { CircleMarker, MapContainer, Popup, TileLayer, Tooltip, useMap } = await import('react-leaflet'); const { canvas } = await import('leaflet'); function FitMapToIssues({ items }: { items: DetectionCoordinateItem[] }) { const map = useMap(); useEffect(() => { if (items.length === 0) return; if (items.length === 1) { map.setView([items[0].latitude, items[0].longitude], 17); return; } map.fitBounds( items.map( (item) => [item.latitude, item.longitude] as [number, number], ), { padding: [32, 32], maxZoom: 17, }, ); }, [items, map]); return null; } return function ClientAssignmentIssuesMapInner({ items, classes, selectedDetectionId, startPoint, endPoint, onSelectDetection, onSetStart, onSetEnd, }: ClientAssignmentIssuesMapProps) { const markerRenderer = useMemo(() => canvas({ tolerance: 12 }), []); const displayNames = new Map( classes.map((item) => [item.class_name, item.display_name]), ); const initialCenter: [number, number] = [ items[0].latitude, items[0].longitude, ]; return ( {items.map((item) => { const visual = getDefectVisual(item.class_name); const IssueIcon = visual.icon; const isSelected = selectedDetectionId === item.id; const isStart = startPoint?.detectionId === item.id; const isEnd = endPoint?.detectionId === item.id; const rangeLabel = isStart ? 'A' : isEnd ? 'B' : null; const rangeColor = isStart ? '#16a34a' : isEnd ? '#dc2626' : visual.boundingBoxColor; const isRangeIssue = isStart || isEnd; const displayName = displayNames.get(item.class_name) ?? item.class_name.replaceAll('_', ' '); const point: AssignmentRangePoint = { detectionId: item.id, latitude: item.latitude, longitude: item.longitude, }; return ( onSelectDetection(item.id), }} >

{displayName}

Detection #{item.id}

{rangeLabel ? ( {rangeLabel} ) : null}
Confidence
{Math.round(item.confidence * 100)}%
Latitude
{item.latitude.toFixed(6)}
Longitude
{item.longitude.toFixed(6)}
{rangeLabel ? ( {rangeLabel} ) : null}
); })}
); }; }, { ssr: false, loading: () =>
, }, ); export function AssignmentIssuesMap({ data, isLoading, isFetchingMore, isError, hasMore, startPoint, endPoint, onSetStart, onSetEnd, onLoadMore, onRetry, }: AssignmentIssuesMapProps) { const [selectedDetectionId, setSelectedDetectionId] = useState( null, ); const validItems = useMemo( () => (data?.items ?? []).filter(isValidCoordinate), [data?.items], ); const total = data?.total ?? 0; const loadedCount = Math.min(data?.items.length ?? 0, total); useEffect(() => { if ( selectedDetectionId !== null && !validItems.some((item) => item.id === selectedDetectionId) ) { setSelectedDetectionId(null); } }, [selectedDetectionId, validItems]); return (
Issues on Map Click a marker, then choose Start or End.
{data ? ( {validItems.length} mapped ) : null}
{isLoading ? (
) : isError ? (

Unable to load issue locations.

Check the connection and try again.

) : validItems.length === 0 ? (

No issue locations available.

Detections without valid coordinates cannot be placed on the map.

) : ( <>
Map legend {(data?.classes ?? []).map((item) => { const visual = getDefectVisual(item.class_name); const IssueIcon = visual.icon; return (
{item.display_name}
); })}
Showing {loadedCount} of {total} {hasMore ? ( ) : ( All issues are shown )}
)} ); }