import { Separator } from "@/components/ui/separator"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PolygonCanvas } from "./PolygonCanvas"; import { Polygon } from "@/types/canvas"; import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil"; import { isDesktop } from "react-device-detect"; import ZoneControls, { NewZoneButton, ZoneObjectSelector, } from "./NewZoneButton"; import { Skeleton } from "../ui/skeleton"; import { useResizeObserver } from "@/hooks/resize-observer"; import { LuCopy, LuPencil, LuPlusSquare, LuTrash } from "react-icons/lu"; import { FaDrawPolygon } from "react-icons/fa"; const parseCoordinates = (coordinatesString: string) => { const coordinates = coordinatesString.split(","); const points = []; for (let i = 0; i < coordinates.length; i += 2) { const x = parseFloat(coordinates[i]); const y = parseFloat(coordinates[i + 1]); points.push([x, y]); } return points; }; export type ZoneObjects = { camera: string; zoneName: string; objects: string[]; }; type MasksAndZoneProps = { selectedCamera: string; setSelectedCamera: React.Dispatch>; }; export default function MasksAndZones({ selectedCamera, setSelectedCamera, }: MasksAndZoneProps) { const { data: config } = useSWR("config"); const [zonePolygons, setZonePolygons] = useState([]); const [zoneObjects, setZoneObjects] = useState([]); const [activePolygonIndex, setActivePolygonIndex] = useState( null, ); const containerRef = useRef(null); const cameras = useMemo(() => { if (!config) { return []; } return Object.values(config.cameras) .filter((conf) => conf.ui.dashboard && conf.enabled) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); }, [config]); const cameraConfig = useMemo(() => { if (config && selectedCamera) { return config.cameras[selectedCamera]; } }, [config, selectedCamera]); const allLabels = useMemo(() => { if (!cameras) { return []; } const labels = new Set(); cameras.forEach((camera) => { camera.objects.track.forEach((label) => { labels.add(label); }); }); return [...labels].sort(); }, [cameras]); // const saveZoneObjects = useCallback( // (camera: string, zoneName: string, newObjects?: string[]) => { // setZoneObjects((prevZoneObjects) => // prevZoneObjects.map((zoneObject) => { // if ( // zoneObject.camera === camera && // zoneObject.zoneName === zoneName // ) { // console.log("found", camera, "with", zoneName); // console.log("new objects", newObjects); // console.log("new zoneobject", { // ...zoneObject, // objects: newObjects ?? [], // }); // // Replace objects with newObjects if provided // return { // ...zoneObject, // objects: newObjects ?? [], // }; // } // return zoneObject; // Keep original object // }), // ); // }, // [setZoneObjects], // ); const saveZoneObjects = useCallback( (camera: string, zoneName: string, objects?: string[]) => { setZoneObjects((prevZoneObjects) => { const updatedZoneObjects = prevZoneObjects.map((zoneObject) => { if ( zoneObject.camera === camera && zoneObject.zoneName === zoneName ) { return { ...zoneObject, objects: objects || [] }; } return zoneObject; }); return updatedZoneObjects; }); }, [setZoneObjects], ); const growe = useMemo(() => { if (!cameraConfig) { return; } const aspectRatio = cameraConfig.detect.width / cameraConfig.detect.height; if (aspectRatio > 2) { return "aspect-wide"; } else if (aspectRatio < 16 / 9) { if (isDesktop) { return "size-full aspect-tall"; } else { return "size-full"; } } else { return "size-full aspect-video"; } }, [cameraConfig]); const getCameraAspect = useCallback( (cam: string) => { if (!config) { return undefined; } const camera = config.cameras[cam]; if (!camera) { return undefined; } return camera.detect.width / camera.detect.height; }, [config], ); const mainCameraAspect = useMemo(() => { const aspectRatio = getCameraAspect(selectedCamera); if (!aspectRatio) { return "normal"; } else if (aspectRatio > 2) { return "wide"; } else if (aspectRatio < 16 / 9) { return "tall"; } else { return "normal"; } }, [getCameraAspect, selectedCamera]); const grow = useMemo(() => { if (mainCameraAspect == "wide") { return "w-full aspect-wide"; } else if (mainCameraAspect == "tall") { if (isDesktop) { return "size-full aspect-tall flex flex-col justify-center"; } else { return "size-full"; } } else { return "w-full aspect-video"; } }, [mainCameraAspect]); const [{ width: containerWidth, height: containerHeight }] = useResizeObserver(containerRef); const { width, height } = cameraConfig ? cameraConfig.detect : { width: 1, height: 1 }; const aspectRatio = width / height; const stretch = true; const fitAspect = 16 / 9; // console.log(containerRef.current?.clientHeight); const scaledHeight = useMemo(() => { const scaledHeight = aspectRatio < (fitAspect ?? 0) ? Math.floor( Math.min(containerHeight, containerRef.current?.clientHeight), ) : Math.floor(containerWidth / aspectRatio); const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height); if (finalHeight > 0) { return finalHeight; } return 100; }, [ aspectRatio, containerWidth, containerHeight, fitAspect, height, stretch, ]); const scaledWidth = useMemo( () => Math.ceil(scaledHeight * aspectRatio), [scaledHeight, aspectRatio], ); useEffect(() => { if (cameraConfig && containerRef.current) { setZonePolygons( Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ camera: cameraConfig.name, name, points: interpolatePoints( parseCoordinates(zoneData.coordinates), 1, 1, scaledWidth, scaledHeight, ), isFinished: true, color: zoneData.color, })), ); setZoneObjects( Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ camera: cameraConfig.name, zoneName: name, objects: Object.keys(zoneData.filters), })), ); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [cameraConfig, containerRef]); useEffect(() => { console.log( "config zone objects", Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ camera: cameraConfig.name, zoneName: name, objects: Object.keys(zoneData.filters), })), ); console.log("component zone objects", zoneObjects); }, [zoneObjects]); useEffect(() => { if (selectedCamera) { setActivePolygonIndex(null); } }, [selectedCamera]); if (!cameraConfig && !selectedCamera) { return ; } return ( <> {cameraConfig && (
Zones
{zonePolygons.map((polygon, index) => (
{polygon.name}
setActivePolygonIndex(index)} >
{ setZonePolygons((oldPolygons) => { return oldPolygons.filter((_, i) => i !== index); }); setActivePolygonIndex(null); }} >
))} {/* Name Coordinates Edit {zonePolygons.map((polygon, index) => ( {polygon.name} {JSON.stringify( interpolatePoints( polygon.points, scaledWidth, scaledHeight, cameraConfig.detect.width, cameraConfig.detect.height, ), null, 0, )}
setActivePolygonIndex(index)} >
saveZoneObjects(polygon.camera, polygon.name, objects) } />
))}
scaled width: {scaledWidth}, scaled height: {scaledHeight}, container width: {containerWidth}, container height: {containerHeight}
                {JSON.stringify(
                  zonePolygons &&
                    zonePolygons.map((polygon) =>
                      interpolatePoints(
                        polygon.points,
                        scaledWidth,
                        scaledHeight,
                        1,
                        1,
                      ),
                    ),
                  null,
                  0,
                )}
              
*/}
{cameraConfig ? ( ) : ( )}
)} ); }