import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../ui/alert-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { LuCopy, LuPencil } from "react-icons/lu"; import { FaDrawPolygon, FaObjectGroup } from "react-icons/fa"; import { BsPersonBoundingBox } from "react-icons/bs"; import { HiOutlineDotsVertical, HiTrash } from "react-icons/hi"; import { isMobile } from "react-device-detect"; import { flattenPoints, parseCoordinates, toRGBColorString, } from "@/utils/canvasUtil"; import { Polygon, PolygonType } from "@/types/canvas"; import { useCallback, useMemo, useState } from "react"; import axios from "axios"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { reviewQueries } from "@/utils/zoneEdutUtil"; type PolygonItemProps = { polygon: Polygon; setAllPolygons: React.Dispatch>; index: number; activePolygonIndex: number | undefined; hoveredPolygonIndex: number | null; setHoveredPolygonIndex: (index: number | null) => void; setActivePolygonIndex: (index: number | undefined) => void; setEditPane: (type: PolygonType) => void; handleCopyCoordinates: (index: number) => void; }; export default function PolygonItem({ polygon, setAllPolygons, index, activePolygonIndex, hoveredPolygonIndex, setHoveredPolygonIndex, setActivePolygonIndex, setEditPane, handleCopyCoordinates, }: PolygonItemProps) { const { data: config, mutate: updateConfig } = useSWR("config"); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const cameraConfig = useMemo(() => { if (polygon?.camera && config) { return config.cameras[polygon.camera]; } }, [polygon, config]); const polygonTypeIcons = { zone: FaDrawPolygon, motion_mask: FaObjectGroup, object_mask: BsPersonBoundingBox, }; const PolygonItemIcon = polygon ? polygonTypeIcons[polygon.type] : undefined; const saveToConfig = useCallback( async (polygon: Polygon) => { if (!polygon || !cameraConfig) { return; } let url = ""; if (polygon.type == "zone") { const { alertQueries, detectionQueries } = reviewQueries( polygon.name, false, false, polygon.camera, cameraConfig?.review.alerts.required_zones || [], cameraConfig?.review.detections.required_zones || [], ); url = `cameras.${polygon.camera}.zones.${polygon.name}${alertQueries}${detectionQueries}`; } if (polygon.type == "motion_mask") { console.log("deleting", polygon.typeIndex); const filteredMask = ( Array.isArray(cameraConfig.motion.mask) ? cameraConfig.motion.mask : [cameraConfig.motion.mask] ).filter((_, currentIndex) => currentIndex !== polygon.typeIndex); console.log(filteredMask); url = filteredMask .map((pointsArray) => { const coordinates = flattenPoints( parseCoordinates(pointsArray), ).join(","); return `cameras.${polygon?.camera}.motion.mask=${coordinates}&`; }) .join(""); if (!url) { // deleting last mask url = `cameras.${polygon?.camera}.motion.mask&`; } console.log(url); // return; // url = `config/set?cameras.${polygon.camera}.motion.mask`; } if (polygon.type == "object_mask") { console.log("deleting", polygon.typeIndex, polygon); let configObject; let globalMask = false; console.log("polygon objects", polygon.objects, !polygon.objects); // global mask on camera for all objects if (!polygon.objects.length) { console.log("deleting global"); configObject = cameraConfig.objects.mask; globalMask = true; } else { configObject = cameraConfig.objects.filters[polygon.objects[0]].mask; } if (!configObject) { return; } const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask) ? cameraConfig.objects.mask : cameraConfig.objects.mask ? [cameraConfig.objects.mask] : []; let filteredMask; if (globalMask) { filteredMask = ( Array.isArray(configObject) ? configObject : [configObject] ).filter((_, currentIndex) => currentIndex !== polygon.typeIndex); } else { console.log("not globals config object:", configObject); filteredMask = ( Array.isArray(configObject) ? configObject : [configObject] ) .filter((mask) => !globalObjectMasksArray.includes(mask)) .filter((_, currentIndex) => { console.log( "current index", currentIndex, "global length:", globalObjectMasksArray.length, "polygon typeindex", polygon.typeIndex, ); return currentIndex !== polygon.typeIndex; }); } console.log("filtered:", filteredMask); url = filteredMask .map((pointsArray) => { const coordinates = flattenPoints( parseCoordinates(pointsArray), ).join(","); return globalMask ? `cameras.${polygon?.camera}.objects.mask=${coordinates}&` : `cameras.${polygon?.camera}.objects.filters.${polygon.objects[0]}.mask=${coordinates}&`; }) .join(""); if (!url) { // deleting last mask url = globalMask ? `cameras.${polygon?.camera}.objects.mask&` : `cameras.${polygon?.camera}.objects.filters.${polygon.objects[0]}.mask`; } console.log("url:", url); // return; // url = `config/set?cameras.${polygon.camera}.motion.mask`; } await axios .put(`config/set?${url}`, { requires_restart: 0 }) .then((res) => { if (res.status === 200) { toast.success(`${polygon?.name} has been deleted.`, { position: "top-center", }); // setChangedValue(false); updateConfig(); } else { toast.error(`Failed to save config changes: ${res.statusText}`, { position: "top-center", }); } }) .catch((error) => { toast.error( `Failed to save config changes: ${error.response.data.message}`, { position: "top-center" }, ); }) .finally(() => { // setIsLoading(false); }); }, [updateConfig, cameraConfig], ); const reindexPolygons = (arr: Polygon[]): Polygon[] => { const typeCounters: { [type: string]: number } = {}; return arr.map((obj) => { if (!typeCounters[obj.type]) { typeCounters[obj.type] = 0; } const newObj: Polygon = { ...obj, typeIndex: typeCounters[obj.type], }; typeCounters[obj.type]++; return newObj; }); }; const handleDelete = (type: string, typeIndex: number) => { // setAllPolygons((oldPolygons) => { // console.log("old polygons", oldPolygons); // const filteredPolygons = oldPolygons.filter( // (polygon) => // !(polygon.type === type && polygon.typeIndex === typeIndex), // ); // console.log("filtered", filteredPolygons); // // console.log("reindexed", reindexPolygons(filteredPolygons)); // return filteredPolygons; // }); setActivePolygonIndex(undefined); saveToConfig(polygon); }; return ( <>
setHoveredPolygonIndex(index)} onMouseLeave={() => setHoveredPolygonIndex(null)} style={{ backgroundColor: hoveredPolygonIndex === index ? toRGBColorString(polygon.color, false) : "", }} >
{PolygonItemIcon && ( )}

{polygon.name}

setDeleteDialogOpen(!deleteDialogOpen)} > Confirm Delete Are you sure you want to delete the{" "} {polygon.type.replace("_", " ")} {polygon.name}? Cancel handleDelete(polygon.type, polygon.typeIndex)} > Delete {isMobile && ( <> { setActivePolygonIndex(index); setEditPane(polygon.type); }} > Edit handleCopyCoordinates(index)}> Copy setDeleteDialogOpen(true)}> Delete )} {!isMobile && hoveredPolygonIndex === index && (
{ setActivePolygonIndex(index); setEditPane(polygon.type); }} > Edit
handleCopyCoordinates(index)} > Copy coordinates
setDeleteDialogOpen(true)} > Delete
)}
); }