diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/components/settings/MasksAndZones.tsx index 035a16927..2ab042bc4 100644 --- a/web/src/components/settings/MasksAndZones.tsx +++ b/web/src/components/settings/MasksAndZones.tsx @@ -7,7 +7,7 @@ import { Polygon, PolygonType } from "@/types/canvas"; import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil"; import { Skeleton } from "../ui/skeleton"; import { useResizeObserver } from "@/hooks/resize-observer"; -import { LuExternalLink, LuInfo, LuPlus } from "react-icons/lu"; +import { LuExternalLink, LuPlus } from "react-icons/lu"; import { HoverCard, HoverCardContent, @@ -25,12 +25,6 @@ import ObjectMaskEditPane from "./ObjectMaskEditPane"; import PolygonItem from "./PolygonItem"; import { Link } from "react-router-dom"; -// export type ZoneObjects = { -// camera: string; -// zoneName: string; -// objects: string[]; -// }; - type MasksAndZoneProps = { selectedCamera: string; selectedZoneMask?: PolygonType[]; @@ -209,12 +203,12 @@ export default function MasksAndZones({ setActivePolygonIndex(allPolygons.length); let polygonColor = [128, 128, 0]; + if (type == "motion_mask") { polygonColor = [0, 0, 220]; } if (type == "object_mask") { polygonColor = [128, 128, 128]; - // TODO - get this from config object after mutation so label can be set } setEditingPolygons([ @@ -224,6 +218,7 @@ export default function MasksAndZones({ isFinished: false, // isUnsaved: true, type, + typeIndex: 9999, name: "", objects: [], camera: selectedCamera, @@ -305,30 +300,49 @@ export default function MasksAndZones({ }), ); - // this can be an array or a string - const motionMasks = Object.entries( - Array.isArray(cameraConfig.motion.mask) - ? cameraConfig.motion.mask - : [cameraConfig.motion.mask], - ).map(([, maskData], index) => ({ - type: "motion_mask" as PolygonType, - typeIndex: index, - camera: cameraConfig.name, - name: `Motion Mask ${index + 1}`, - objects: [], - points: interpolatePoints( - parseCoordinates(maskData), - 1, - 1, - scaledWidth, - scaledHeight, - ), - isFinished: true, - color: [0, 0, 255], - })); + let motionMasks: Polygon[] = []; + let globalObjectMasks: Polygon[] = []; + let objectMasks: Polygon[] = []; - const globalObjectMasks = Object.entries(cameraConfig.objects.mask).map( - ([, maskData], index) => ({ + if ( + cameraConfig.motion.mask !== null && + cameraConfig.motion.mask !== undefined + ) { + // this can be an array or a string + motionMasks = ( + Array.isArray(cameraConfig.motion.mask) + ? cameraConfig.motion.mask + : cameraConfig.motion.mask + ? [cameraConfig.motion.mask] + : [] + ).map((maskData, index) => ({ + type: "motion_mask" as PolygonType, + typeIndex: index, + camera: cameraConfig.name, + name: `Motion Mask ${index + 1}`, + objects: [], + points: interpolatePoints( + parseCoordinates(maskData), + 1, + 1, + scaledWidth, + scaledHeight, + ), + isFinished: true, + color: [0, 0, 255], + })); + } + + const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask) + ? cameraConfig.objects.mask + : cameraConfig.objects.mask + ? [cameraConfig.objects.mask] + : []; + if ( + cameraConfig.objects.mask !== null && + cameraConfig.objects.mask !== undefined + ) { + globalObjectMasks = globalObjectMasksArray.map((maskData, index) => ({ type: "object_mask" as PolygonType, typeIndex: index, camera: cameraConfig.name, @@ -342,41 +356,66 @@ export default function MasksAndZones({ scaledHeight, ), isFinished: true, - // isUnsaved: false, - color: [0, 0, 255], - }), - ); + color: [128, 128, 128], + })); + } + + // if (globalObjectMasks && !Array.isArray(globalObjectMasks)) { + // globalObjectMasks = [globalObjectMasks]; + // } + + console.log("global", globalObjectMasks); const globalObjectMasksCount = globalObjectMasks.length; - const objectMasks = Object.entries(cameraConfig.objects.filters).flatMap( - ([objectName, { mask }]): Polygon[] => - mask !== null && mask !== undefined - ? mask.flatMap((maskItem, subIndex) => - maskItem !== null && maskItem !== undefined - ? [ - { - type: "object_mask" as PolygonType, - typeIndex: subIndex, - camera: cameraConfig.name, - name: `Object Mask ${globalObjectMasksCount + subIndex + 1} (${objectName})`, - objects: [objectName], - points: interpolatePoints( - parseCoordinates(maskItem), - 1, - 1, - scaledWidth, - scaledHeight, - ), - isFinished: true, - // isUnsaved: false, - color: [128, 128, 128], - }, - ] - : [], + console.log("filters", cameraConfig.objects.filters); + + let index = 0; + objectMasks = Object.entries(cameraConfig.objects.filters) + .filter(([_, { mask }]) => mask || Array.isArray(mask)) + .flatMap(([objectName, { mask }]): Polygon[] => { + console.log("index", index); + console.log("outer", objectName, mask); + + const maskArray = Array.isArray(mask) ? mask : mask ? [mask] : []; + + return maskArray.flatMap((maskItem, subIndex) => { + const maskItemString = maskItem; + + const newMask = { + type: "object_mask" as PolygonType, + typeIndex: subIndex, + camera: cameraConfig.name, + name: `Object Mask ${globalObjectMasksCount + index + 1} (${objectName})`, + objects: [objectName], + points: interpolatePoints( + parseCoordinates(maskItem), + 1, + 1, + scaledWidth, + scaledHeight, + ), + isFinished: true, + color: [128, 128, 128], + }; + index++; + + if ( + globalObjectMasksArray.some( + (globalMask) => globalMask === maskItemString, ) - : [], - ); + ) { + index--; + return []; + } else { + return [newMask]; + } + }); + }); + + console.log(Object.entries(cameraConfig.objects.filters)); + + console.log("final object masks", objectMasks); // console.log("setting all and editing"); setAllPolygons([ @@ -404,9 +443,9 @@ export default function MasksAndZones({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [cameraConfig, containerRef, scaledHeight, scaledWidth]); - useEffect(() => { - console.log("editing polygons changed:", editingPolygons); - }, [editingPolygons]); + // useEffect(() => { + // console.log("editing polygons changed:", editingPolygons); + // }, [editingPolygons]); useEffect(() => { if (editPane === undefined) { diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx index a921af8ed..ff7a32a7e 100644 --- a/web/src/components/settings/MotionMaskEditPane.tsx +++ b/web/src/components/settings/MotionMaskEditPane.tsx @@ -19,6 +19,7 @@ import { import axios from "axios"; import { toast } from "sonner"; import { Toaster } from "../ui/sonner"; +import ActivityIndicator from "../indicators/activity-indicator"; type MotionMaskEditPaneProps = { polygons?: Polygon[]; @@ -105,7 +106,9 @@ export default function MotionMaskEditPane({ let index = Array.isArray(cameraConfig.motion.mask) ? cameraConfig.motion.mask.length - : 1; + : cameraConfig.motion.mask + ? 1 + : 0; console.log("are we an array?", Array.isArray(cameraConfig.motion.mask)); console.log("index", index); @@ -114,20 +117,14 @@ export default function MotionMaskEditPane({ // editing existing mask, not creating a new one if (editingMask) { index = polygon.typeIndex; - if (polygon.name) { - const match = polygon.name.match(/\d+/); - if (match) { - // index = parseInt(match[0]) - 1; - console.log("editing, index", index); - } - } + console.log("editing, index", index); } - const filteredMask = Array.isArray(cameraConfig.motion.mask) - ? cameraConfig.motion.mask - : [cameraConfig.motion.mask].filter( - (_, currentIndex) => currentIndex !== index, - ); + const filteredMask = ( + Array.isArray(cameraConfig.motion.mask) + ? cameraConfig.motion.mask + : [cameraConfig.motion.mask] + ).filter((_, currentIndex) => currentIndex !== index); console.log("filtered", filteredMask); // if (editingMask) { @@ -163,7 +160,7 @@ export default function MotionMaskEditPane({ }) .then((res) => { if (res.status === 200) { - toast.success(`Zone ${name} saved.`, { + toast.success(`${polygon.name || "Motion Mask"} has been saved.`, { position: "top-center", }); // setChangedValue(false); @@ -274,8 +271,20 @@ export default function MotionMaskEditPane({ - diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx index 706e59c19..143539cf5 100644 --- a/web/src/components/settings/ObjectMaskEditPane.tsx +++ b/web/src/components/settings/ObjectMaskEditPane.tsx @@ -19,20 +19,33 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { Polygon } from "@/types/canvas"; +import { ObjectMaskFormValuesType, Polygon } from "@/types/canvas"; import PolygonEditControls from "./PolygonEditControls"; import { FaCheckCircle } from "react-icons/fa"; +import { + flattenPoints, + interpolatePoints, + parseCoordinates, +} from "@/utils/canvasUtil"; +import axios from "axios"; +import { toast } from "sonner"; +import { Toaster } from "../ui/sonner"; +import ActivityIndicator from "../indicators/activity-indicator"; type ObjectMaskEditPaneProps = { polygons?: Polygon[]; setPolygons: React.Dispatch>; activePolygonIndex?: number; + scaledWidth?: number; + scaledHeight?: number; + isLoading: boolean; + setIsLoading: React.Dispatch>; onSave?: () => void; onCancel?: () => void; }; @@ -41,9 +54,15 @@ export default function ObjectMaskEditPane({ polygons, setPolygons, activePolygonIndex, + scaledWidth, + scaledHeight, + isLoading, + setIsLoading, onSave, onCancel, }: ObjectMaskEditPaneProps) { + const { data: config, mutate: updateConfig } = + useSWR("config"); // const { data: config } = useSWR("config"); // const cameras = useMemo(() => { @@ -64,6 +83,12 @@ export default function ObjectMaskEditPane({ } }, [polygons, activePolygonIndex]); + const cameraConfig = useMemo(() => { + if (polygon?.camera && config) { + return config.cameras[polygon.camera]; + } + }, [polygon, config]); + const defaultName = useMemo(() => { if (!polygons) { return; @@ -101,20 +126,157 @@ export default function ObjectMaskEditPane({ }, }); + const saveToConfig = useCallback( + async ( + { objects: form_objects }: ObjectMaskFormValuesType, // values submitted via the form + objects: string[], + ) => { + if (!scaledWidth || !scaledHeight || !polygon || !cameraConfig) { + return; + } + // console.log("loitering time", loitering_time); + // const alertsZones = config?.cameras[camera]?.review.alerts.required_zones; + + // const detectionsZones = + // config?.cameras[camera]?.review.detections.required_zones; + + // console.log("out of try except", mutatedConfig); + + console.log("form objects:", form_objects); + console.log("objects:", objects); + console.log(cameraConfig.objects.filters); + + const coordinates = flattenPoints( + interpolatePoints(polygon.points, scaledWidth, scaledHeight, 1, 1), + ).join(","); + + let queryString = ""; + let configObject; + let createFilter = false; + let globalMask = false; + let filteredMask = [coordinates]; + const editingMask = polygon.name.length > 0; + + // global mask on camera for all objects + if (form_objects == "all_labels") { + configObject = cameraConfig.objects.mask; + globalMask = true; + } else { + if ( + cameraConfig.objects.filters[form_objects] && + cameraConfig.objects.filters[form_objects].mask !== null + ) { + configObject = cameraConfig.objects.filters[form_objects].mask; + } else { + createFilter = true; + } + } + + if (!createFilter) { + let index = Array.isArray(configObject) + ? configObject.length + : configObject + ? 1 + : 0; + + if (editingMask) { + index = polygon.typeIndex; + } + + console.log("are we an array?", Array.isArray(configObject)); + console.log("index", index); + + // editing existing mask, not creating a new one + if (editingMask) { + index = polygon.typeIndex; + } + + filteredMask = ( + Array.isArray(configObject) ? configObject : [configObject as string] + ).filter((_, currentIndex) => currentIndex !== index); + + console.log("filtered", filteredMask); + + filteredMask.splice(index, 0, coordinates); + console.log("filtered after splice", filteredMask); + } + + queryString = filteredMask + .map((pointsArray) => { + const coordinates = flattenPoints(parseCoordinates(pointsArray)).join( + ",", + ); + return globalMask + ? `cameras.${polygon?.camera}.objects.mask=${coordinates}&` + : `cameras.${polygon?.camera}.objects.filters.${form_objects}.mask=${coordinates}&`; + }) + .join(""); + + console.log("polygon", polygon); + console.log(queryString); + + // console.log( + // `config/set?cameras.${polygon?.camera}.objects.mask=${coordinates}&${queryString}`, + // ); + // console.log("object masks", cameraConfig.objects.mask); + // console.log("new coords", coordinates); + // return; + + if (!queryString) { + console.log("no query string"); + return; + } + + axios + .put(`config/set?${queryString}`, { + requires_restart: 0, + }) + .then((res) => { + if (res.status === 200) { + toast.success(`${polygon.name || "Object Mask"} has been saved.`, { + 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, polygon, scaledWidth, scaledHeight, setIsLoading], + ); + function onSubmit(values: z.infer) { - console.log("form values", values); - // if (activePolygonIndex === undefined || !polygons) { - // return; - // } + if (activePolygonIndex === undefined || !values || !polygons) { + return; + } + setIsLoading(true); + // polygons[activePolygonIndex].name = values.name; + // console.log("form values", values); + // console.log( + // "string", - // const updatedPolygons = [...polygons]; - // const activePolygon = updatedPolygons[activePolygonIndex]; - // updatedPolygons[activePolygonIndex] = { - // ...activePolygon, - // name: defaultName ?? "foo", - // }; - // setPolygons(updatedPolygons); + // flattenPoints( + // interpolatePoints(polygon.points, scaledWidth, scaledHeight, 1, 1), + // ).join(","), + // ); + // console.log("active polygon", polygons[activePolygonIndex]); + saveToConfig( + values as ObjectMaskFormValuesType, + polygons[activePolygonIndex].objects, + ); if (onSave) { onSave(); } @@ -126,6 +288,7 @@ export default function ObjectMaskEditPane({ return ( <> + {polygon.name.length ? "Edit" : "New"} Object Mask @@ -211,8 +374,20 @@ export default function ObjectMaskEditPane({ - diff --git a/web/src/components/settings/PolygonDrawer.tsx b/web/src/components/settings/PolygonDrawer.tsx index 7a9294cf3..c9b824ac7 100644 --- a/web/src/components/settings/PolygonDrawer.tsx +++ b/web/src/components/settings/PolygonDrawer.tsx @@ -106,6 +106,7 @@ export default function PolygonDrawer({ onMouseOver={isActive ? handleGroupMouseOver : undefined} onTouchStart={isActive ? handleGroupMouseOver : undefined} onMouseOut={isActive ? handleGroupMouseOut : undefined} + zIndex={isActive ? 999 : 100} > @@ -51,6 +55,7 @@ export default function PolygonEditControls({