2024-04-08 04:57:15 +03:00
|
|
|
import { Polygon } from "@/types/canvas";
|
2024-04-09 16:46:26 +03:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
2024-04-10 20:03:46 +03:00
|
|
|
DialogDescription,
|
2024-04-09 16:46:26 +03:00
|
|
|
DialogFooter,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@/components/ui/dialog";
|
2024-04-10 20:03:46 +03:00
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
|
|
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|
|
|
|
import { useMemo, useState } from "react";
|
2024-04-09 16:46:26 +03:00
|
|
|
import { Input } from "../ui/input";
|
2024-04-10 20:03:46 +03:00
|
|
|
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
|
|
|
|
|
import { FaObjectGroup } from "react-icons/fa";
|
|
|
|
|
import { Button } from "../ui/button";
|
|
|
|
|
import { ATTRIBUTES, FrigateConfig } from "@/types/frigateConfig";
|
2024-04-09 16:46:26 +03:00
|
|
|
import useSWR from "swr";
|
2024-04-10 20:03:46 +03:00
|
|
|
import { isMobile } from "react-device-detect";
|
|
|
|
|
|
|
|
|
|
type ZoneObjectSelectorProps = {
|
|
|
|
|
camera: string;
|
|
|
|
|
zoneName: string;
|
|
|
|
|
allLabels: string[];
|
|
|
|
|
updateLabelFilter: (labels: string[] | undefined) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function ZoneObjectSelector({
|
|
|
|
|
camera,
|
|
|
|
|
zoneName,
|
|
|
|
|
allLabels,
|
|
|
|
|
updateLabelFilter,
|
|
|
|
|
}: ZoneObjectSelectorProps) {
|
|
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const cameraConfig = useMemo(() => {
|
|
|
|
|
if (config && camera) {
|
|
|
|
|
return config.cameras[camera];
|
|
|
|
|
}
|
|
|
|
|
}, [config, camera]);
|
|
|
|
|
|
|
|
|
|
const zoneLabels = useMemo<string[]>(() => {
|
|
|
|
|
if (!cameraConfig || !zoneName) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const labels = new Set<string>();
|
|
|
|
|
|
|
|
|
|
cameraConfig.objects.track.forEach((label) => {
|
|
|
|
|
if (!ATTRIBUTES.includes(label)) {
|
|
|
|
|
labels.add(label);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-04-11 07:08:34 +03:00
|
|
|
if (cameraConfig.zones[zoneName]) {
|
|
|
|
|
cameraConfig.zones[zoneName].objects.forEach((label) => {
|
|
|
|
|
if (!ATTRIBUTES.includes(label)) {
|
|
|
|
|
labels.add(label);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-04-10 20:03:46 +03:00
|
|
|
|
|
|
|
|
return [...labels].sort() || [];
|
|
|
|
|
}, [cameraConfig, zoneName]);
|
|
|
|
|
|
|
|
|
|
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
|
|
|
|
zoneLabels,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const trigger = (
|
|
|
|
|
<Button
|
|
|
|
|
className={`flex items-center gap-2 capitalize ${false ? "bg-selected hover:bg-selected" : ""}`}
|
|
|
|
|
size="sm"
|
|
|
|
|
>
|
|
|
|
|
<FaObjectGroup
|
|
|
|
|
className={`${false ? "text-background dark:text-primary" : "text-secondary-foreground"}`}
|
|
|
|
|
/>
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const content = (
|
|
|
|
|
<GeneralFilterContent
|
|
|
|
|
allLabels={allLabels}
|
|
|
|
|
selectedLabels={zoneLabels}
|
|
|
|
|
currentLabels={currentLabels}
|
|
|
|
|
updateLabelFilter={updateLabelFilter}
|
|
|
|
|
setCurrentLabels={setCurrentLabels}
|
|
|
|
|
onClose={() => setOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isMobile) {
|
|
|
|
|
return (
|
|
|
|
|
<Drawer
|
|
|
|
|
open={open}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setCurrentLabels(zoneLabels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setOpen(open);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
|
|
|
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
|
|
|
|
{content}
|
|
|
|
|
</DrawerContent>
|
|
|
|
|
</Drawer>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Popover
|
|
|
|
|
open={open}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setCurrentLabels(zoneLabels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setOpen(open);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
|
|
|
<PopoverContent>{content}</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-04-08 04:57:15 +03:00
|
|
|
|
2024-04-10 20:03:46 +03:00
|
|
|
type ZoneControlsProps = {
|
2024-04-08 04:57:15 +03:00
|
|
|
camera: string;
|
|
|
|
|
polygons: Polygon[];
|
|
|
|
|
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
|
|
|
|
activePolygonIndex: number | null;
|
|
|
|
|
setActivePolygonIndex: React.Dispatch<React.SetStateAction<number | null>>;
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-10 20:03:46 +03:00
|
|
|
export function ZoneControls({
|
|
|
|
|
camera,
|
2024-04-08 04:57:15 +03:00
|
|
|
polygons,
|
|
|
|
|
setPolygons,
|
|
|
|
|
activePolygonIndex,
|
|
|
|
|
setActivePolygonIndex,
|
2024-04-10 20:03:46 +03:00
|
|
|
}: ZoneControlsProps) {
|
2024-04-09 16:46:26 +03:00
|
|
|
const { data: config } = useSWR("config");
|
|
|
|
|
const [zoneName, setZoneName] = useState<string | null>();
|
|
|
|
|
const [invalidName, setInvalidName] = useState<boolean>();
|
|
|
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
|
|
|
|
2024-04-08 04:57:15 +03:00
|
|
|
const undo = () => {
|
|
|
|
|
if (activePolygonIndex !== null && polygons) {
|
|
|
|
|
const updatedPolygons = [...polygons];
|
|
|
|
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
|
|
|
|
if (activePolygon.points.length > 0) {
|
|
|
|
|
updatedPolygons[activePolygonIndex] = {
|
|
|
|
|
...activePolygon,
|
|
|
|
|
points: activePolygon.points.slice(0, -1),
|
|
|
|
|
isFinished: false,
|
|
|
|
|
};
|
|
|
|
|
setPolygons(updatedPolygons);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const reset = () => {
|
|
|
|
|
if (activePolygonIndex !== null) {
|
|
|
|
|
const updatedPolygons = [...polygons];
|
|
|
|
|
updatedPolygons[activePolygonIndex] = {
|
|
|
|
|
points: [],
|
|
|
|
|
isFinished: false,
|
2024-04-11 07:08:34 +03:00
|
|
|
name: updatedPolygons[activePolygonIndex].name,
|
2024-04-10 20:03:46 +03:00
|
|
|
camera: camera,
|
2024-04-10 03:04:23 +03:00
|
|
|
color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0],
|
2024-04-08 04:57:15 +03:00
|
|
|
};
|
|
|
|
|
setPolygons(updatedPolygons);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-09 16:46:26 +03:00
|
|
|
const handleNewPolygon = (zoneName: string) => {
|
2024-04-08 04:57:15 +03:00
|
|
|
setPolygons([
|
|
|
|
|
...(polygons || []),
|
|
|
|
|
{
|
|
|
|
|
points: [],
|
|
|
|
|
isFinished: false,
|
2024-04-09 16:46:26 +03:00
|
|
|
name: zoneName,
|
2024-04-10 20:03:46 +03:00
|
|
|
camera: camera,
|
2024-04-10 03:04:23 +03:00
|
|
|
color: [220, 0, 0],
|
2024-04-08 04:57:15 +03:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
setActivePolygonIndex(polygons.length);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<div className="flex justify-between items-center my-5">
|
|
|
|
|
<Button className="mr-5" variant="secondary" onClick={undo}>
|
|
|
|
|
Undo
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="secondary" onClick={reset}>
|
|
|
|
|
Reset
|
|
|
|
|
</Button>
|
2024-04-09 16:46:26 +03:00
|
|
|
<Button variant="secondary" onClick={() => setDialogOpen(true)}>
|
|
|
|
|
New Zone
|
2024-04-08 04:57:15 +03:00
|
|
|
</Button>
|
2024-04-09 16:46:26 +03:00
|
|
|
<Dialog
|
|
|
|
|
open={dialogOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
setDialogOpen(open);
|
|
|
|
|
if (!open) {
|
|
|
|
|
setZoneName("");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogContent>
|
2024-04-11 07:08:34 +03:00
|
|
|
{isMobile && <span tabIndex={0} className="sr-only" />}
|
2024-04-09 16:46:26 +03:00
|
|
|
<DialogTitle>New Zone</DialogTitle>
|
2024-04-10 20:03:46 +03:00
|
|
|
<DialogDescription>
|
2024-04-11 07:08:34 +03:00
|
|
|
Enter a unique label for your zone. Do not include spaces, and
|
|
|
|
|
don't use the name of a camera.
|
2024-04-10 20:03:46 +03:00
|
|
|
</DialogDescription>
|
2024-04-09 16:46:26 +03:00
|
|
|
<>
|
|
|
|
|
<Input
|
2024-04-11 07:08:34 +03:00
|
|
|
className={`mt-3 ${isMobile && "text-md"}`}
|
2024-04-09 16:46:26 +03:00
|
|
|
type="search"
|
|
|
|
|
value={zoneName ?? ""}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setInvalidName(
|
2024-04-10 20:03:46 +03:00
|
|
|
Object.keys(config.cameras).includes(e.target.value) ||
|
2024-04-11 07:08:34 +03:00
|
|
|
e.target.value.includes(" ") ||
|
|
|
|
|
polygons
|
|
|
|
|
.map((item) => item.name)
|
|
|
|
|
.includes(e.target.value),
|
2024-04-09 16:46:26 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setZoneName(e.target.value);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{invalidName && (
|
2024-04-11 07:08:34 +03:00
|
|
|
<div className="text-danger text-sm">Invalid zone name.</div>
|
2024-04-09 16:46:26 +03:00
|
|
|
)}
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="select"
|
|
|
|
|
disabled={invalidName || (zoneName?.length ?? 0) == 0}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (zoneName) {
|
|
|
|
|
setDialogOpen(false);
|
|
|
|
|
handleNewPolygon(zoneName);
|
|
|
|
|
}
|
|
|
|
|
setZoneName(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Continue
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2024-04-08 04:57:15 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-10 20:03:46 +03:00
|
|
|
export default ZoneControls;
|