import Heading from "../ui/heading"; import { Separator } from "../ui/separator"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { useEffect, useMemo, useState } from "react"; import { ATTRIBUTES, FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import { isMobile } from "react-device-detect"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Polygon } from "@/types/canvas"; import { Switch } from "../ui/switch"; import { Label } from "../ui/label"; import PolygonEditControls from "./PolygonEditControls"; type ZoneEditPaneProps = { polygons?: Polygon[]; setPolygons: React.Dispatch>; activePolygonIndex?: number; onSave?: () => void; onCancel?: () => void; }; export function ZoneEditPane({ polygons, setPolygons, activePolygonIndex, onSave, onCancel, }: ZoneEditPaneProps) { const { data: config } = useSWR("config"); 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 polygon = useMemo(() => { if (polygons && activePolygonIndex !== undefined) { return polygons[activePolygonIndex]; } else { return null; } }, [polygons, activePolygonIndex]); const formSchema = z .object({ name: z .string() .min(2, { message: "Zone name must be at least 2 characters.", }) .transform((val: string) => val.trim().replace(/\s+/g, "_")) .refine( (value: string) => { return !cameras.map((cam) => cam.name).includes(value); }, { message: "Zone name must not be the name of a camera.", }, ) .refine( (value: string) => { const otherPolygonNames = polygons ?.filter((_, index) => index !== activePolygonIndex) .map((polygon) => polygon.name) || []; return !otherPolygonNames.includes(value); }, { message: "Zone name already exists on this camera.", }, ), inertia: z.coerce.number().min(1, { message: "Inertia must be above 0.", }), loitering_time: z.coerce.number().min(0, { message: "Loitering time must be greater than or equal to 0.", }), polygon: z.object({ isFinished: z.boolean() }), }) .refine(() => polygon?.isFinished === true, { message: "The polygon drawing must be finished before saving.", path: ["polygon.isFinished"], }); const form = useForm>({ resolver: zodResolver(formSchema), mode: "onChange", defaultValues: { name: polygon?.name ?? "", inertia: ((polygon?.camera && polygon?.name && config?.cameras[polygon.camera]?.zones[polygon.name] ?.inertia) as number) || 3, loitering_time: ((polygon?.camera && polygon?.name && config?.cameras[polygon.camera]?.zones[polygon.name] ?.loitering_time) as number) || 0, polygon: { isFinished: polygon?.isFinished ?? false }, }, }); function onSubmit(values: z.infer) { polygons[activePolygonIndex].name = values.name; console.log("form values", values); console.log("active polygon", polygons[activePolygonIndex]); // make sure polygon isFinished onSave(); } if (!polygon) { return; } return ( <> Zone {polygons && activePolygonIndex !== undefined && (
{polygons[activePolygonIndex].points.length} points
{polygons[activePolygonIndex].isFinished ? <> : <>}
)}
Click to draw a polygon on the image.
( Name Name must be at least 2 characters and must not be the name of a camera or another zone. )} />
( Inertia Specifies how many frames that an object must be in a zone before they are considered in the zone. )} />
( Loitering Time Sets a minimum amount of time in seconds that the object must be in the zone for it to activate. )} />
Objects List of objects that apply to this zone. { // console.log(objects); }} />
Alerts and Detections When an object enters this zone, ensure it is marked as an alert or detection.
{ if (isChecked) { return; } }} />
{ if (isChecked) { return; } }} />
( )} />
); } type ZoneObjectSelectorProps = { camera: string; zoneName: string; updateLabelFilter: (labels: string[] | undefined) => void; }; export function ZoneObjectSelector({ camera, zoneName, updateLabelFilter, }: ZoneObjectSelectorProps) { const { data: config } = useSWR("config"); const cameraConfig = useMemo(() => { if (config && camera) { return config.cameras[camera]; } }, [config, camera]); const allLabels = useMemo(() => { if (!config) { return []; } const labels = new Set(); Object.values(config.cameras).forEach((camera) => { camera.objects.track.forEach((label) => { if (!ATTRIBUTES.includes(label)) { labels.add(label); } }); }); return [...labels].sort(); }, [config]); const zoneLabels = useMemo(() => { if (!cameraConfig || !zoneName) { return []; } const labels = new Set(); cameraConfig.objects.track.forEach((label) => { if (!ATTRIBUTES.includes(label)) { labels.add(label); } }); if (cameraConfig.zones[zoneName]) { cameraConfig.zones[zoneName].objects.forEach((label) => { if (!ATTRIBUTES.includes(label)) { labels.add(label); } }); } return [...labels].sort() || []; }, [cameraConfig, zoneName]); const [currentLabels, setCurrentLabels] = useState( zoneLabels.every((label, index) => label === allLabels[index]) ? undefined : zoneLabels, ); useEffect(() => { updateLabelFilter(currentLabels); }, [currentLabels, updateLabelFilter]); return ( <>
{ if (isChecked) { setCurrentLabels(undefined); } }} />
{allLabels.map((item) => (
{ if (isChecked) { const updatedLabels = currentLabels ? [...currentLabels] : []; updatedLabels.push(item); setCurrentLabels(updatedLabels); } else { const updatedLabels = currentLabels ? [...currentLabels] : []; // can not deselect the last item if (updatedLabels.length > 1) { updatedLabels.splice(updatedLabels.indexOf(item), 1); setCurrentLabels(updatedLabels); } } }} />
))}
); }