131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { AlertTriangle, ImageIcon, Loader2 } from 'lucide-react';
|
|
import Image from 'next/image';
|
|
|
|
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
|
import { useProtectedMediaObjectUrl } from '@/hooks/useProtectedMedia';
|
|
import type { BoundingBoxOverlayItem, DetectionResultItem } from '@/types';
|
|
|
|
import BoundingBoxOverlay from '@/components/annotation/boundingBoxOverlay';
|
|
|
|
interface AnnotatedDetectionImageProps {
|
|
detection?: DetectionResultItem;
|
|
videoWidth: number;
|
|
videoHeight: number;
|
|
aspectRatio?: string;
|
|
enableLightbox?: boolean;
|
|
}
|
|
|
|
export default function AnnotatedDetectionImage({
|
|
detection,
|
|
videoWidth,
|
|
videoHeight,
|
|
aspectRatio: aspectRatioOverride,
|
|
enableLightbox = false,
|
|
}: AnnotatedDetectionImageProps) {
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
const { objectUrl, isLoading, isError } = useProtectedMediaObjectUrl(
|
|
detection?.detection.context_image_url,
|
|
);
|
|
const overlayDetections: BoundingBoxOverlayItem[] = detection
|
|
? [
|
|
{
|
|
id: detection.id,
|
|
class_name: detection.detection.class_name,
|
|
display_name: detection.detection.display_name,
|
|
confidence: detection.detection.confidence,
|
|
bounding_box: detection.detection.bounding_box,
|
|
},
|
|
]
|
|
: [];
|
|
|
|
const aspectRatio =
|
|
aspectRatioOverride ??
|
|
(videoWidth > 0 && videoHeight > 0
|
|
? `${videoWidth} / ${videoHeight}`
|
|
: '16 / 9');
|
|
|
|
const annotatedMedia = objectUrl ? (
|
|
<>
|
|
<Image
|
|
src={objectUrl}
|
|
alt={`${detection?.detection.display_name ?? 'Road defect'} detection`}
|
|
fill
|
|
unoptimized
|
|
sizes="(min-width: 1280px) 18rem, (min-width: 640px) 50vw, 100vw"
|
|
className="object-contain"
|
|
/>
|
|
<BoundingBoxOverlay
|
|
detections={overlayDetections}
|
|
videoWidth={videoWidth}
|
|
videoHeight={videoHeight}
|
|
/>
|
|
</>
|
|
) : null;
|
|
|
|
if (detection && objectUrl && enableLightbox) {
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsPreviewOpen(true)}
|
|
aria-label={`Open ${detection.detection.display_name} detection image`}
|
|
className="group relative flex w-full cursor-zoom-in items-center justify-center overflow-hidden rounded-lg border bg-black focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
style={{ aspectRatio }}
|
|
>
|
|
<Image
|
|
src={objectUrl}
|
|
alt={`${detection.detection.display_name} detection`}
|
|
fill
|
|
unoptimized
|
|
sizes="(min-width: 1280px) 18rem, (min-width: 640px) 50vw, 100vw"
|
|
className="object-contain"
|
|
/>
|
|
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-[3] bg-gradient-to-t from-black/70 to-transparent px-3 pb-2 pt-8 text-center text-xs font-medium text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
|
Click to view annotated image
|
|
</span>
|
|
</button>
|
|
|
|
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
|
<DialogContent className="h-[calc(100vh-2rem)] bg-black/95 p-4 text-white sm:max-w-[calc(100vw-2rem)]">
|
|
<DialogTitle className="sr-only">
|
|
{detection.detection.display_name} annotated detection image
|
|
</DialogTitle>
|
|
<div className="relative min-h-0 w-full flex-1 overflow-hidden rounded-lg bg-black">
|
|
{annotatedMedia}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="relative flex w-full items-center justify-center overflow-hidden rounded-lg border bg-black"
|
|
style={{ aspectRatio }}
|
|
>
|
|
{!detection ? (
|
|
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
<ImageIcon className="size-7" />
|
|
<p>Select a detection to view its image.</p>
|
|
</div>
|
|
) : isLoading ? (
|
|
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
<Loader2 className="size-7 animate-spin text-primary" />
|
|
<p>Loading detection image...</p>
|
|
</div>
|
|
) : isError || !objectUrl ? (
|
|
<div className="flex flex-col items-center gap-2 text-destructive">
|
|
<AlertTriangle className="size-7" />
|
|
<p>Failed to load the detection image.</p>
|
|
</div>
|
|
) : (
|
|
annotatedMedia
|
|
)}
|
|
</div>
|
|
);
|
|
}
|