import { useState, useEffect, useMemo, useRef } from "react"; import useSWR from "swr"; import { useApiHost } from "@/api"; import type { SearchResult } from "@/types/search"; import { ObjectPath } from "./ObjectPath"; import type { FrigateConfig } from "@/types/frigateConfig"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent } from "@/components/ui/card"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { useTimezone } from "@/hooks/use-date-utils"; import { Button } from "@/components/ui/button"; import { LuX } from "react-icons/lu"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; export default function ObjectPathPlotter() { const apiHost = useApiHost(); const [timeRange, setTimeRange] = useState("1d"); const { data: config } = useSWR("config"); const imgRef = useRef(null); const timezone = useTimezone(config); const [selectedCamera, setSelectedCamera] = useState(""); const [selectedEvent, setSelectedEvent] = useState(null); const [currentPage, setCurrentPage] = useState(1); const eventsPerPage = 20; useEffect(() => { if (config && !selectedCamera) { setSelectedCamera(Object.keys(config.cameras)[0]); } }, [config, selectedCamera]); const searchQuery = useMemo(() => { if (!selectedCamera) return null; return [ "events", { cameras: selectedCamera, after: Math.floor(Date.now() / 1000) - getTimeRangeInSeconds(timeRange), before: Math.floor(Date.now() / 1000), has_clip: 1, include_thumbnails: 0, limit: 1000, timezone, }, ]; }, [selectedCamera, timeRange, timezone]); const { data: events } = useSWR(searchQuery); const aspectRatio = useMemo(() => { if (!config || !selectedCamera) return 16 / 9; return ( config.cameras[selectedCamera].detect.width / config.cameras[selectedCamera].detect.height ); }, [config, selectedCamera]); const pathPoints = useMemo(() => { if (!events) return []; return events.flatMap( (event) => event.data.path_data?.map( ([coords, timestamp]: [number[], number]) => ({ x: coords[0], y: coords[1], timestamp, event, }), ) || [], ); }, [events]); const getRandomColor = () => { return [ Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), ]; }; const eventColors = useMemo(() => { if (!events) return {}; return events.reduce( (acc, event) => { acc[event.id] = getRandomColor(); return acc; }, {} as Record, ); }, [events]); const [imageLoaded, setImageLoaded] = useState(false); useEffect(() => { if (!selectedCamera) return; const img = new Image(); img.src = selectedEvent ? `${apiHost}api/${selectedCamera}/recordings/${selectedEvent.start_time}/snapshot.jpg` : `${apiHost}api/${selectedCamera}/latest.jpg?h=500`; img.onload = () => { if (imgRef.current) { imgRef.current.src = img.src; setImageLoaded(true); } }; }, [apiHost, selectedCamera, selectedEvent]); const handleEventClick = (event: SearchResult) => { setSelectedEvent(event.id === selectedEvent?.id ? null : event); }; const clearSelectedEvent = () => { setSelectedEvent(null); }; const totalPages = Math.ceil((events?.length || 0) / eventsPerPage); const paginatedEvents = events?.slice( (currentPage - 1) * eventsPerPage, currentPage * eventsPerPage, ); return (

Tracked Object Paths

{`Latest {imgRef.current && imageLoaded && ( {events?.map((event) => ( point.event.id === event.id, )} color={eventColors[event.id]} width={2} imgRef={imgRef} visible={ selectedEvent === null || selectedEvent.id === event.id } /> ))} )}

Legend

{selectedEvent && ( )}
{paginatedEvents?.map((event) => (
handleEventClick(event)} >
{event.label} {formatUnixTimestampToDateTime(event.start_time, { timezone: config?.ui.timezone, })}
))}
setCurrentPage((prev) => Math.max(prev - 1, 1)) } /> {[...Array(totalPages)].map((_, index) => ( setCurrentPage(index + 1)} isActive={currentPage === index + 1} > {index + 1} ))} setCurrentPage((prev) => Math.min(prev + 1, totalPages)) } />
); } function getTimeRangeInSeconds(range: string): number { switch (range) { case "1h": return 60 * 60; case "6h": return 6 * 60 * 60; case "12h": return 12 * 60 * 60; case "1d": return 24 * 60 * 60; default: return 24 * 60 * 60; } }