This commit is contained in:
Josh Hawkins 2024-04-10 23:08:34 -05:00
parent cf96f7b706
commit 8d0385c991
6 changed files with 203 additions and 97 deletions

View File

@ -67,8 +67,8 @@ export function PolygonCanvas({
return distance < 15; return distance < 15;
}; };
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => { const handleMouseDown = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (!activePolygonIndex || !polygons) { if (activePolygonIndex == null || !polygons) {
return; return;
} }
@ -100,50 +100,63 @@ export function PolygonCanvas({
// } // }
}; };
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => { const handleMouseOverStartPoint = (
if (activePolygonIndex !== null && polygons) { e: KonvaEventObject<MouseEvent | TouchEvent>,
const activePolygon = polygons[activePolygonIndex]; ) => {
if (!activePolygon.isFinished && activePolygon.points.length >= 3) { if (activePolygonIndex == null || !polygons) {
e.currentTarget.scale({ x: 2, y: 2 }); return;
} }
const activePolygon = polygons[activePolygonIndex];
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
e.currentTarget.scale({ x: 2, y: 2 });
} }
}; };
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => { const handleMouseOutStartPoint = (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => {
e.currentTarget.scale({ x: 1, y: 1 }); e.currentTarget.scale({ x: 1, y: 1 });
if (activePolygonIndex !== null && polygons) {
const activePolygon = polygons[activePolygonIndex]; if (activePolygonIndex == null || !polygons) {
if ( return;
(!activePolygon.isFinished && activePolygon.points.length >= 3) || }
activePolygon.isFinished
) { const activePolygon = polygons[activePolygonIndex];
e.currentTarget.scale({ x: 1, y: 1 }); if (
} (!activePolygon.isFinished && activePolygon.points.length >= 3) ||
activePolygon.isFinished
) {
e.currentTarget.scale({ x: 1, y: 1 });
} }
}; };
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => { const handlePointDragMove = (
if (activePolygonIndex !== null && polygons) { e: KonvaEventObject<MouseEvent | TouchEvent>,
const updatedPolygons = [...polygons]; ) => {
const activePolygon = updatedPolygons[activePolygonIndex]; if (activePolygonIndex == null || !polygons) {
const stage = e.target.getStage(); return;
if (stage) { }
const index = e.target.index - 1;
const pos = [e.target._lastPos!.x, e.target._lastPos!.y]; const updatedPolygons = [...polygons];
if (pos[0] < 0) pos[0] = 0; const activePolygon = updatedPolygons[activePolygonIndex];
if (pos[1] < 0) pos[1] = 0; const stage = e.target.getStage();
if (pos[0] > stage.width()) pos[0] = stage.width(); if (stage) {
if (pos[1] > stage.height()) pos[1] = stage.height(); const index = e.target.index - 1;
updatedPolygons[activePolygonIndex] = { const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
...activePolygon, if (pos[0] < 0) pos[0] = 0;
points: [ if (pos[1] < 0) pos[1] = 0;
...activePolygon.points.slice(0, index), if (pos[0] > stage.width()) pos[0] = stage.width();
pos, if (pos[1] > stage.height()) pos[1] = stage.height();
...activePolygon.points.slice(index + 1), updatedPolygons[activePolygonIndex] = {
], ...activePolygon,
}; points: [
setPolygons(updatedPolygons); ...activePolygon.points.slice(0, index),
} pos,
...activePolygon.points.slice(index + 1),
],
};
setPolygons(updatedPolygons);
} }
}; };
@ -151,7 +164,7 @@ export function PolygonCanvas({
return points.reduce((acc, point) => [...acc, ...point], []); return points.reduce((acc, point) => [...acc, ...point], []);
}; };
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => { const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (activePolygonIndex !== null && e.target.name() === "polygon") { if (activePolygonIndex !== null && e.target.name() === "polygon") {
const updatedPolygons = [...polygons]; const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex]; const activePolygon = updatedPolygons[activePolygonIndex];
@ -174,6 +187,7 @@ export function PolygonCanvas({
width={width} width={width}
height={height} height={height}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
> >
<Layer> <Layer>
<Image <Image

View File

@ -11,10 +11,14 @@ type PolygonDrawerProps = {
isActive: boolean; isActive: boolean;
isFinished: boolean; isFinished: boolean;
color: number[]; color: number[];
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void; handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void; handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleMouseOverStartPoint: (e: KonvaEventObject<MouseEvent>) => void; handleMouseOverStartPoint: (
handleMouseOutStartPoint: (e: KonvaEventObject<MouseEvent>) => void; e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
handleMouseOutStartPoint: (
e: KonvaEventObject<MouseEvent | TouchEvent>,
) => void;
}; };
export default function PolygonDrawer({ export default function PolygonDrawer({
@ -33,13 +37,17 @@ export default function PolygonDrawer({
const [minMaxX, setMinMaxX] = useState([0, 0]); const [minMaxX, setMinMaxX] = useState([0, 0]);
const [minMaxY, setMinMaxY] = useState([0, 0]); const [minMaxY, setMinMaxY] = useState([0, 0]);
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => { const handleGroupMouseOver = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (!isFinished) return; if (!isFinished) return;
e.target.getStage()!.container().style.cursor = "move"; e.target.getStage()!.container().style.cursor = "move";
setStage(e.target.getStage()!); setStage(e.target.getStage()!);
}; };
const handleGroupMouseOut = (e: Konva.KonvaEventObject<MouseEvent>) => { const handleGroupMouseOut = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
) => {
if (!e.target) return; if (!e.target) return;
e.target.getStage()!.container().style.cursor = "default"; e.target.getStage()!.container().style.cursor = "default";
}; };
@ -87,6 +95,7 @@ export default function PolygonDrawer({
onDragEnd={isActive ? handleGroupDragEnd : undefined} onDragEnd={isActive ? handleGroupDragEnd : undefined}
dragBoundFunc={isActive ? groupDragBound : undefined} dragBoundFunc={isActive ? groupDragBound : undefined}
onMouseOver={isActive ? handleGroupMouseOver : undefined} onMouseOver={isActive ? handleGroupMouseOver : undefined}
onTouchStart={isActive ? handleGroupMouseOver : undefined}
onMouseOut={isActive ? handleGroupMouseOut : undefined} onMouseOut={isActive ? handleGroupMouseOut : undefined}
> >
<Line <Line

View File

@ -43,11 +43,8 @@ export function ZoneObjectSelector({
if (!cameraConfig || !zoneName) { if (!cameraConfig || !zoneName) {
return []; return [];
} }
console.log(zoneName);
const labels = new Set<string>(); const labels = new Set<string>();
// console.log("zone name", zoneName);
// console.log(cameraConfig.zones[zoneName].objects);
cameraConfig.objects.track.forEach((label) => { cameraConfig.objects.track.forEach((label) => {
if (!ATTRIBUTES.includes(label)) { if (!ATTRIBUTES.includes(label)) {
@ -55,9 +52,13 @@ export function ZoneObjectSelector({
} }
}); });
cameraConfig.zones[zoneName].objects.forEach((label) => { if (cameraConfig.zones[zoneName]) {
labels.add(label); cameraConfig.zones[zoneName].objects.forEach((label) => {
}); if (!ATTRIBUTES.includes(label)) {
labels.add(label);
}
});
}
return [...labels].sort() || []; return [...labels].sort() || [];
}, [cameraConfig, zoneName]); }, [cameraConfig, zoneName]);
@ -166,7 +167,7 @@ export function ZoneControls({
updatedPolygons[activePolygonIndex] = { updatedPolygons[activePolygonIndex] = {
points: [], points: [],
isFinished: false, isFinished: false,
name: "new", name: updatedPolygons[activePolygonIndex].name,
camera: camera, camera: camera,
color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0], color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0],
}; };
@ -210,29 +211,31 @@ export function ZoneControls({
}} }}
> >
<DialogContent> <DialogContent>
{isMobile && <span tabIndex={0} className="sr-only" />}
<DialogTitle>New Zone</DialogTitle> <DialogTitle>New Zone</DialogTitle>
<DialogDescription> <DialogDescription>
Enter a label for your zone. Do not include spaces, and don't use Enter a unique label for your zone. Do not include spaces, and
the name of a camera. don't use the name of a camera.
</DialogDescription> </DialogDescription>
<> <>
<Input <Input
className="mt-3" className={`mt-3 ${isMobile && "text-md"}`}
type="search" type="search"
value={zoneName ?? ""} value={zoneName ?? ""}
onChange={(e) => { onChange={(e) => {
setInvalidName( setInvalidName(
Object.keys(config.cameras).includes(e.target.value) || Object.keys(config.cameras).includes(e.target.value) ||
e.target.value.includes(" "), e.target.value.includes(" ") ||
polygons
.map((item) => item.name)
.includes(e.target.value),
); );
setZoneName(e.target.value); setZoneName(e.target.value);
}} }}
/> />
{invalidName && ( {invalidName && (
<div className="text-danger text-sm"> <div className="text-danger text-sm">Invalid zone name.</div>
Zone name is not valid.
</div>
)} )}
<DialogFooter> <DialogFooter>
<Button <Button

View File

@ -20,7 +20,7 @@ import {
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { PolygonCanvas } from "./PolygonCanvas"; import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon } from "@/types/canvas"; import { Polygon } from "@/types/canvas";
import { interpolatePoints } from "@/utils/canvasUtil"; import { interpolatePoints } from "@/utils/canvasUtil";
@ -43,9 +43,16 @@ const parseCoordinates = (coordinatesString: string) => {
return points; return points;
}; };
export type ZoneObjects = {
camera: string;
zoneName: string;
objects: string[];
};
export default function SettingsZones() { export default function SettingsZones() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]); const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
const [zoneObjects, setZoneObjects] = useState<ZoneObjects[]>([]);
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>( const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
null, null,
); );
@ -85,6 +92,51 @@ export default function SettingsZones() {
return [...labels].sort(); return [...labels].sort();
}, [cameras]); }, [cameras]);
// 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],
);
const grow = useMemo(() => { const grow = useMemo(() => {
if (!cameraConfig) { if (!cameraConfig) {
return; return;
@ -105,6 +157,14 @@ export default function SettingsZones() {
} }
}, [cameraConfig]); }, [cameraConfig]);
const handleCameraChange = useCallback(
(camera: string) => {
setSelectedCamera(camera);
setActivePolygonIndex(null);
},
[setSelectedCamera, setActivePolygonIndex],
);
const [{ width: containerWidth, height: containerHeight }] = const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef); useResizeObserver(containerRef);
@ -159,20 +219,40 @@ export default function SettingsZones() {
color: zoneData.color, color: zoneData.color,
})), })),
); );
setZoneObjects(
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
camera: cameraConfig.name,
zoneName: name,
objects: Object.keys(zoneData.filters),
})),
);
} }
// we know that these deps are correct // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [cameraConfig, containerRef]); }, [cameraConfig, containerRef]);
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]);
if (!cameraConfig && !selectedCamera) { if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
return ( return (
<> <div className="overflow-auto">
<Heading as="h2">Zones</Heading> <Heading as="h2">Zones</Heading>
<div className="flex items-center space-x-2 mt-5"> <div className="flex items-center space-x-2 mt-5">
<Select value={selectedCamera} onValueChange={setSelectedCamera}> <Select value={selectedCamera} onValueChange={handleCameraChange}>
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Camera" /> <SelectValue placeholder="Camera" />
</SelectTrigger> </SelectTrigger>
@ -194,9 +274,9 @@ export default function SettingsZones() {
</div> </div>
{cameraConfig && ( {cameraConfig && (
<div className="flex flex-row justify-evenly"> <div className="flex flex-col justify-evenly">
<div <div
className={`flex flex-col justify-center items-center w-[60%] ${grow}`} className={`flex flex-col justify-center items-center w-full md:w-[60%] ${grow}`}
> >
<div ref={containerRef} className="size-full"> <div ref={containerRef} className="size-full">
{cameraConfig ? ( {cameraConfig ? (
@ -213,7 +293,7 @@ export default function SettingsZones() {
)} )}
</div> </div>
</div> </div>
<div className="w-[30%]"> <div className="w-full md:w-[30%]">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -254,7 +334,9 @@ export default function SettingsZones() {
camera={polygon.camera} camera={polygon.camera}
zoneName={polygon.name} zoneName={polygon.name}
allLabels={allLabels} allLabels={allLabels}
updateLabelFilter={(objects) => console.log(objects)} updateLabelFilter={(objects) =>
saveZoneObjects(polygon.camera, polygon.name, objects)
}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -294,6 +376,6 @@ export default function SettingsZones() {
</div> </div>
</div> </div>
)} )}
</> </div>
); );
} }

View File

@ -46,27 +46,31 @@ function General() {
export default function Settings() { export default function Settings() {
return ( return (
<> <div className="w-full h-full">
<Tabs defaultValue="general" className="w-auto"> <div className="flex h-full">
<TabsList> <div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5">
<TabsTrigger value="general">General</TabsTrigger> <Tabs defaultValue="general" className="w-auto">
<TabsTrigger value="objects">Objects</TabsTrigger> <TabsList>
<TabsTrigger value="zones">Zones</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="masks">Masks</TabsTrigger> <TabsTrigger value="objects">Objects</TabsTrigger>
<TabsTrigger value="motion">Motion</TabsTrigger> <TabsTrigger value="zones">Zones</TabsTrigger>
</TabsList> <TabsTrigger value="masks">Masks</TabsTrigger>
<TabsContent value="general"> <TabsTrigger value="motion">Motion</TabsTrigger>
<General /> </TabsList>
</TabsContent> <TabsContent value="general">
<TabsContent value="objects">Objects</TabsContent> <General />
<TabsContent value="zones"> </TabsContent>
<SettingsZones /> <TabsContent value="objects">Objects</TabsContent>
</TabsContent> <TabsContent value="zones">
<TabsContent value="masks">Masks</TabsContent> <SettingsZones />
<TabsContent value="motion"> </TabsContent>
<MotionTuner /> <TabsContent value="masks">Masks</TabsContent>
</TabsContent> <TabsContent value="motion">
</Tabs> <MotionTuner />
</> </TabsContent>
</Tabs>
</div>
</div>
</div>
); );
} }

View File

@ -21,13 +21,7 @@ export interface BirdseyeConfig {
width: number; width: number;
} }
export const ATTRIBUTES = [ export const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
"amazon",
"face",
"fedex",
"license_plate",
"ups",
] as const;
export interface CameraConfig { export interface CameraConfig {
audio: { audio: {