frigate/web/src/components/settings/MasksAndZones.tsx

471 lines
14 KiB
TypeScript
Raw Normal View History

2024-04-11 23:48:35 +03:00
import { Separator } from "@/components/ui/separator";
2024-04-08 19:02:35 +03:00
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
2024-04-10 20:03:46 +03:00
2024-04-05 15:25:23 +03:00
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import ActivityIndicator from "@/components/indicators/activity-indicator";
2024-04-11 07:08:34 +03:00
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2024-04-08 04:57:15 +03:00
import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon } from "@/types/canvas";
2024-04-11 23:48:35 +03:00
import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil";
2024-04-08 04:57:15 +03:00
import { isDesktop } from "react-device-detect";
2024-04-11 23:48:35 +03:00
import ZoneControls, {
NewZoneButton,
ZoneObjectSelector,
} from "./NewZoneButton";
2024-04-08 04:57:15 +03:00
import { Skeleton } from "../ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
2024-04-11 23:48:35 +03:00
import { LuCopy, LuPencil, LuPlusSquare, LuTrash } from "react-icons/lu";
import { FaDrawPolygon } from "react-icons/fa";
2024-04-08 04:57:15 +03:00
const parseCoordinates = (coordinatesString: string) => {
const coordinates = coordinatesString.split(",");
const points = [];
for (let i = 0; i < coordinates.length; i += 2) {
2024-04-10 03:04:23 +03:00
const x = parseFloat(coordinates[i]);
const y = parseFloat(coordinates[i + 1]);
2024-04-08 04:57:15 +03:00
points.push([x, y]);
}
return points;
};
2024-04-05 15:25:23 +03:00
2024-04-11 07:08:34 +03:00
export type ZoneObjects = {
camera: string;
zoneName: string;
objects: string[];
};
2024-04-11 23:48:35 +03:00
type MasksAndZoneProps = {
selectedCamera: string;
setSelectedCamera: React.Dispatch<React.SetStateAction<string>>;
};
export default function MasksAndZones({
selectedCamera,
setSelectedCamera,
}: MasksAndZoneProps) {
2024-04-05 15:25:23 +03:00
const { data: config } = useSWR<FrigateConfig>("config");
2024-04-08 04:57:15 +03:00
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
2024-04-11 07:08:34 +03:00
const [zoneObjects, setZoneObjects] = useState<ZoneObjects[]>([]);
2024-04-08 04:57:15 +03:00
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
null,
);
const containerRef = useRef<HTMLDivElement | null>(null);
2024-04-05 15:25:23 +03:00
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]);
2024-04-10 20:03:46 +03:00
const allLabels = useMemo<string[]>(() => {
if (!cameras) {
return [];
}
const labels = new Set<string>();
cameras.forEach((camera) => {
camera.objects.track.forEach((label) => {
labels.add(label);
});
});
return [...labels].sort();
}, [cameras]);
2024-04-11 07:08:34 +03:00
// 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],
);
2024-04-11 23:48:35 +03:00
const growe = useMemo(() => {
2024-04-08 04:57:15 +03:00
if (!cameraConfig) {
return;
}
const aspectRatio = cameraConfig.detect.width / cameraConfig.detect.height;
2024-04-09 05:35:53 +03:00
if (aspectRatio > 2) {
2024-04-08 04:57:15 +03:00
return "aspect-wide";
2024-04-09 05:35:53 +03:00
} else if (aspectRatio < 16 / 9) {
2024-04-08 04:57:15 +03:00
if (isDesktop) {
return "size-full aspect-tall";
} else {
return "size-full";
}
} else {
2024-04-09 05:35:53 +03:00
return "size-full aspect-video";
2024-04-08 04:57:15 +03:00
}
2024-04-09 05:35:53 +03:00
}, [cameraConfig]);
2024-04-08 19:02:35 +03:00
2024-04-11 23:48:35 +03:00
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;
2024-04-11 07:08:34 +03:00
},
2024-04-11 23:48:35 +03:00
[config],
2024-04-11 07:08:34 +03:00
);
2024-04-11 23:48:35 +03:00
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]);
2024-04-09 05:35:53 +03:00
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);
2024-04-08 04:57:15 +03:00
const { width, height } = cameraConfig
? cameraConfig.detect
: { width: 1, height: 1 };
const aspectRatio = width / height;
2024-04-11 23:48:35 +03:00
const stretch = true;
const fitAspect = 16 / 9;
// console.log(containerRef.current?.clientHeight);
2024-04-09 05:35:53 +03:00
2024-04-08 04:57:15 +03:00
const scaledHeight = useMemo(() => {
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
2024-04-11 23:48:35 +03:00
? Math.floor(
Math.min(containerHeight, containerRef.current?.clientHeight),
)
2024-04-09 05:35:53 +03:00
: Math.floor(containerWidth / aspectRatio);
2024-04-08 04:57:15 +03:00
const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height);
if (finalHeight > 0) {
return finalHeight;
}
return 100;
}, [
aspectRatio,
2024-04-09 05:35:53 +03:00
containerWidth,
2024-04-08 04:57:15 +03:00
containerHeight,
fitAspect,
height,
stretch,
]);
2024-04-09 05:35:53 +03:00
2024-04-08 04:57:15 +03:00
const scaledWidth = useMemo(
2024-04-09 05:35:53 +03:00
() => Math.ceil(scaledHeight * aspectRatio),
[scaledHeight, aspectRatio],
2024-04-08 04:57:15 +03:00
);
useEffect(() => {
if (cameraConfig && containerRef.current) {
setZonePolygons(
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
2024-04-10 20:03:46 +03:00
camera: cameraConfig.name,
2024-04-08 04:57:15 +03:00
name,
points: interpolatePoints(
parseCoordinates(zoneData.coordinates),
2024-04-10 03:04:23 +03:00
1,
1,
2024-04-08 04:57:15 +03:00
scaledWidth,
scaledHeight,
),
isFinished: true,
2024-04-10 03:04:23 +03:00
color: zoneData.color,
2024-04-08 04:57:15 +03:00
})),
);
2024-04-11 07:08:34 +03:00
setZoneObjects(
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
camera: cameraConfig.name,
zoneName: name,
objects: Object.keys(zoneData.filters),
})),
);
2024-04-08 04:57:15 +03:00
}
2024-04-08 19:02:35 +03:00
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cameraConfig, containerRef]);
2024-04-08 04:57:15 +03:00
2024-04-11 07:08:34 +03:00
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]);
2024-04-11 23:48:35 +03:00
useEffect(() => {
if (selectedCamera) {
setActivePolygonIndex(null);
}
}, [selectedCamera]);
2024-04-05 15:25:23 +03:00
if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />;
}
return (
2024-04-11 23:48:35 +03:00
<>
2024-04-08 04:57:15 +03:00
{cameraConfig && (
2024-04-11 23:48:35 +03:00
<div className="flex flex-col md:flex-row size-full">
<div className="flex flex-col order-last w-full md:w-3/12 md:order-none md:mr-2">
<div className="flex mb-3">
<Separator />
2024-04-08 04:57:15 +03:00
</div>
2024-04-11 23:48:35 +03:00
<div className="flex flex-row justify-between items-center mb-3">
<div className="text-md">Zones</div>
<NewZoneButton
camera={cameraConfig.name}
polygons={zonePolygons}
setPolygons={setZonePolygons}
activePolygonIndex={activePolygonIndex}
setActivePolygonIndex={setActivePolygonIndex}
/>
</div>
{zonePolygons.map((polygon, index) => (
<div
key={index}
className="flex p-1 rounded-lg flex-row items-center justify-between mx-2 mb-1"
style={{
backgroundColor:
activePolygonIndex === index
? toRGBColorString(polygon.color, false)
: "",
}}
>
<div
className={`flex items-center ${activePolygonIndex === index ? "text-primary" : "text-secondary-foreground"}`}
>
<FaDrawPolygon
className="size-4 mr-2"
style={{
fill: toRGBColorString(polygon.color, true),
color: toRGBColorString(polygon.color, true),
}}
/>
{polygon.name}
</div>
<div className="flex flex-row gap-2">
<div
className="cursor-pointer"
onClick={() => setActivePolygonIndex(index)}
>
<LuPencil
className={`size-4 ${activePolygonIndex === index ? "text-primary" : "text-secondary-foreground"}`}
/>
</div>
<LuCopy
className={`size-4 ${activePolygonIndex === index ? "text-primary" : "text-secondary-foreground"}`}
/>
<div
className="cursor-pointer"
onClick={() => {
setZonePolygons((oldPolygons) => {
return oldPolygons.filter((_, i) => i !== index);
});
setActivePolygonIndex(null);
}}
>
<LuTrash
className={`size-4 ${activePolygonIndex === index ? "text-primary fill-primary" : "text-secondary-foreground fill-secondary-foreground"}`}
/>
</div>
</div>
</div>
))}
{/* <Table>
2024-04-08 19:02:35 +03:00
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Name</TableHead>
<TableHead className="max-w-[200px]">Coordinates</TableHead>
<TableHead>Edit</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{zonePolygons.map((polygon, index) => (
<TableRow key={index}>
<TableCell className="font-medium">
{polygon.name}
</TableCell>
<TableCell className="max-w-[200px] text-wrap">
<code>
{JSON.stringify(
interpolatePoints(
polygon.points,
scaledWidth,
scaledHeight,
cameraConfig.detect.width,
cameraConfig.detect.height,
),
null,
0,
)}
</code>
</TableCell>
<TableCell>
<div
className="cursor-pointer"
onClick={() => setActivePolygonIndex(index)}
>
<LuPencil className="size-4 text-white" />
</div>
2024-04-10 20:03:46 +03:00
<ZoneObjectSelector
camera={polygon.camera}
zoneName={polygon.name}
allLabels={allLabels}
2024-04-11 07:08:34 +03:00
updateLabelFilter={(objects) =>
saveZoneObjects(polygon.camera, polygon.name, objects)
}
2024-04-10 20:03:46 +03:00
/>
2024-04-08 19:02:35 +03:00
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
2024-04-09 05:35:53 +03:00
<div>
scaled width: {scaledWidth}, scaled height: {scaledHeight},
container width: {containerWidth}, container height:
{containerHeight}
</div>
2024-04-10 20:03:46 +03:00
<ZoneControls
2024-04-08 04:57:15 +03:00
camera={cameraConfig.name}
polygons={zonePolygons}
setPolygons={setZonePolygons}
activePolygonIndex={activePolygonIndex}
setActivePolygonIndex={setActivePolygonIndex}
/>
<div className="flex flex-col justify-center items-center m-auto w-[30%] bg-secondary">
<pre style={{ whiteSpace: "pre-wrap" }}>
{JSON.stringify(
zonePolygons &&
zonePolygons.map((polygon) =>
interpolatePoints(
polygon.points,
scaledWidth,
scaledHeight,
2024-04-08 19:02:35 +03:00
1,
1,
2024-04-08 04:57:15 +03:00
),
),
null,
0,
)}
</pre>
2024-04-11 23:48:35 +03:00
</div> */}
</div>
<div
ref={containerRef}
className="flex md:w-7/12 md:grow md:h-dvh md:max-h-[90%]"
>
<div className="size-full">
{cameraConfig ? (
<PolygonCanvas
camera={cameraConfig.name}
width={scaledWidth}
height={scaledHeight}
polygons={zonePolygons}
setPolygons={setZonePolygons}
activePolygonIndex={activePolygonIndex}
/>
) : (
<Skeleton className="w-full h-full" />
)}
2024-04-08 04:57:15 +03:00
</div>
</div>
</div>
)}
2024-04-11 23:48:35 +03:00
</>
2024-04-05 15:25:23 +03:00
);
}