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

278 lines
8.3 KiB
TypeScript
Raw Normal View History

2024-04-05 15:25:23 +03:00
import Heading from "@/components/ui/heading";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
2024-04-08 19:02:35 +03:00
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
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-09 05:35:53 +03:00
import { useEffect, useMemo, useRef, useState } from "react";
2024-04-08 04:57:15 +03:00
import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon } from "@/types/canvas";
import { interpolatePoints } from "@/utils/canvasUtil";
import { isDesktop } from "react-device-detect";
import PolygonControls from "./PolygonControls";
import { Skeleton } from "../ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
2024-04-08 19:02:35 +03:00
import { LuPencil } from "react-icons/lu";
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) {
const x = parseInt(coordinates[i], 10);
const y = parseInt(coordinates[i + 1], 10);
points.push([x, y]);
}
return points;
};
2024-04-05 15:25:23 +03:00
export default function SettingsZones() {
const { data: config } = useSWR<FrigateConfig>("config");
2024-04-08 04:57:15 +03:00
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
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 [selectedCamera, setSelectedCamera] = useState(cameras[0].name);
const cameraConfig = useMemo(() => {
if (config && selectedCamera) {
return config.cameras[selectedCamera];
}
}, [config, selectedCamera]);
2024-04-09 05:35:53 +03:00
const grow = 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-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-08 19:02:35 +03:00
const stretch = false;
2024-04-09 05:35:53 +03:00
const fitAspect = 0.75;
2024-04-08 04:57:15 +03:00
const scaledHeight = useMemo(() => {
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
? Math.floor(containerHeight)
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]) => ({
name,
points: interpolatePoints(
parseCoordinates(zoneData.coordinates),
cameraConfig.detect.width,
cameraConfig.detect.height,
scaledWidth,
scaledHeight,
),
isFinished: true,
})),
);
}
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-05 15:25:23 +03:00
if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />;
}
return (
<>
2024-04-08 19:02:35 +03:00
<Heading as="h2">Zones</Heading>
2024-04-05 15:25:23 +03:00
<div className="flex items-center space-x-2 mt-5">
<Select value={selectedCamera} onValueChange={setSelectedCamera}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Camera" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Choose a camera</SelectLabel>
{cameras.map((camera) => (
<SelectItem
key={camera.name}
value={`${camera.name}`}
className="capitalize"
>
{camera.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
2024-04-08 04:57:15 +03:00
{cameraConfig && (
<div className="flex flex-row justify-evenly">
<div
2024-04-09 05:35:53 +03:00
className={`flex flex-col justify-center items-center w-[60%] ${grow}`}
2024-04-08 04:57:15 +03:00
>
<div ref={containerRef} className="size-full">
{cameraConfig ? (
<PolygonCanvas
camera={cameraConfig.name}
width={scaledWidth}
height={scaledHeight}
polygons={zonePolygons}
setPolygons={setZonePolygons}
activePolygonIndex={activePolygonIndex}
/>
) : (
<Skeleton className="w-full h-full" />
)}
</div>
</div>
<div className="w-[30%]">
2024-04-08 19:02:35 +03:00
<Table>
<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>
</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-08 04:57:15 +03:00
<PolygonControls
camera={cameraConfig.name}
width={scaledWidth}
height={scaledHeight}
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>
</div>
</div>
</div>
)}
2024-04-05 15:25:23 +03:00
</>
);
}