diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx
index 2f2005d9c..28ad3b883 100644
--- a/web/src/components/camera/AutoUpdatingCameraImage.tsx
+++ b/web/src/components/camera/AutoUpdatingCameraImage.tsx
@@ -6,6 +6,7 @@ type AutoUpdatingCameraImageProps = {
searchParams?: URLSearchParams;
showFps?: boolean;
className?: string;
+ cameraClasses?: string;
reloadInterval?: number;
};
@@ -16,6 +17,7 @@ export default function AutoUpdatingCameraImage({
searchParams = undefined,
showFps = true,
className,
+ cameraClasses,
reloadInterval = MIN_LOAD_TIMEOUT_MS,
}: AutoUpdatingCameraImageProps) {
const [key, setKey] = useState(Date.now());
@@ -68,6 +70,7 @@ export default function AutoUpdatingCameraImage({
camera={camera}
onload={handleLoad}
searchParams={`cache=${key}&${searchParams}`}
+ className={cameraClasses}
/>
{showFps ? Displaying at {fps}fps : null}
diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx
index be978f7a5..1f2c28ade 100644
--- a/web/src/components/camera/CameraImage.tsx
+++ b/web/src/components/camera/CameraImage.tsx
@@ -36,12 +36,7 @@ export default function CameraImage({
}, [apiHost, name, imgRef, searchParams, config]);
return (
-
+
{enabled ? (
diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx
index 925d7c88f..26c8c6477 100644
--- a/web/src/components/player/LivePlayer.tsx
+++ b/web/src/components/player/LivePlayer.tsx
@@ -163,6 +163,7 @@ export default function LivePlayer({
camera={cameraConfig.name}
showFps={false}
reloadInterval={stillReloadInterval}
+ cameraClasses="relative w-full h-full flex justify-center"
/>
diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/components/settings/MasksAndZones.tsx
index 326eb444d..2657812f9 100644
--- a/web/src/components/settings/MasksAndZones.tsx
+++ b/web/src/components/settings/MasksAndZones.tsx
@@ -5,16 +5,19 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon, PolygonType } from "@/types/canvas";
import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil";
-import { isDesktop, isMobile } from "react-device-detect";
+import { isMobile } from "react-device-detect";
import { Skeleton } from "../ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
-import { LuCopy, LuPencil, LuPlusSquare, LuTrash } from "react-icons/lu";
+import { LuCopy, LuPencil, LuPlus } from "react-icons/lu";
import { FaDrawPolygon, FaObjectGroup } from "react-icons/fa";
+import { BsPersonBoundingBox } from "react-icons/bs";
+import { HiTrash } from "react-icons/hi";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { Toaster } from "../ui/sonner";
import { ZoneEditPane } from "./ZoneEditPane";
import { Button } from "../ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import {
AlertDialog,
AlertDialogAction,
@@ -25,8 +28,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
-import { Separator } from "../ui/separator";
-import { BsPersonBoundingBox } from "react-icons/bs";
+import Heading from "../ui/heading";
const parseCoordinates = (coordinatesString: string) => {
const coordinates = coordinatesString.split(",");
@@ -196,7 +198,7 @@ export default function MasksAndZones({
}, [config, selectedCamera]);
const stretch = true;
- const fitAspect = 1;
+ const fitAspect = 16 / 9;
const scaledHeight = useMemo(() => {
if (containerRef.current && aspectRatio && detectHeight) {
@@ -247,13 +249,18 @@ export default function MasksAndZones({
};
const handleCancel = useCallback(() => {
+ console.log("handling cancel");
setEditPane(undefined);
- setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved));
+ console.log("all", allPolygons);
+ console.log("editing", editingPolygons);
+ // setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved));
+ setEditingPolygons([...allPolygons]);
setActivePolygonIndex(undefined);
setHoveredPolygonIndex(null);
- }, [allPolygons]);
+ }, [allPolygons, editingPolygons]);
const handleSave = useCallback(() => {
+ console.log("handling save");
setAllPolygons([...(editingPolygons ?? [])]);
setActivePolygonIndex(undefined);
setEditPane(undefined);
@@ -368,20 +375,27 @@ export default function MasksAndZones({
: [],
);
+ console.log("setting all and editing");
setAllPolygons([
...zones,
...motionMasks,
...globalObjectMasks,
...objectMasks,
]);
+ setEditingPolygons([
+ ...zones,
+ ...motionMasks,
+ ...globalObjectMasks,
+ ...objectMasks,
+ ]);
- setZoneObjects(
- Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
- camera: cameraConfig.name,
- zoneName: name,
- objects: Object.keys(zoneData.filters),
- })),
- );
+ // 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
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -391,23 +405,25 @@ export default function MasksAndZones({
if (editPane === undefined) {
setEditingPolygons([...allPolygons]);
setIsEditing(false);
- console.log(allPolygons);
+ console.log("edit pane undefined, all", allPolygons);
} else {
setIsEditing(true);
}
- }, [setEditingPolygons, setIsEditing, allPolygons, editPane]);
+ // we know that these deps are correct
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [setEditingPolygons, setIsEditing, allPolygons]);
- 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]);
+ // 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]);
useEffect(() => {
if (selectedCamera) {
@@ -421,13 +437,14 @@ export default function MasksAndZones({
return (
<>
- {cameraConfig && allPolygons && (
+ {cameraConfig && editingPolygons && (
-
+
{editPane == "zone" && (
- {(selectedZoneMask === undefined ||
- selectedZoneMask.includes("zone" as PolygonType)) && (
- <>
-
-
Zones
-
{
- setEditPane("zone");
- handleNewPolygon("zone");
- }}
- >
-
-
+
+ Masks / Zones
+
+
+ {(selectedZoneMask === undefined ||
+ selectedZoneMask.includes("zone" as PolygonType)) && (
+
+
+
Zones
+
+
+ {
+ setEditPane("zone");
+ handleNewPolygon("zone");
+ }}
+ >
+
+
+
+ Add Zone
+
+
+ {allPolygons
+ .flatMap((polygon, index) =>
+ polygon.type === "zone" ? [{ polygon, index }] : [],
+ )
+ .map(({ polygon, index }) => (
+
+ ))}
- {allPolygons
- .flatMap((polygon, index) =>
- polygon.type === "zone" ? [{ polygon, index }] : [],
- )
- .map(({ polygon, index }) => (
-
- ))}
- >
- )}
-
-
+ )}
+ {(selectedZoneMask === undefined ||
+ selectedZoneMask.includes(
+ "motion_mask" as PolygonType,
+ )) && (
+
+
+
Motion Masks
+
+
+ {
+ setEditPane("motion_mask");
+ handleNewPolygon("motion_mask");
+ }}
+ >
+
+
+
+ Add Motion Mask
+
+
+ {allPolygons
+ .flatMap((polygon, index) =>
+ polygon.type === "motion_mask"
+ ? [{ polygon, index }]
+ : [],
+ )
+ .map(({ polygon, index }) => (
+
+ ))}
+
+ )}
+ {(selectedZoneMask === undefined ||
+ selectedZoneMask.includes(
+ "object_mask" as PolygonType,
+ )) && (
+
+
+
Object Masks
+
+
+ {
+ setEditPane("object_mask");
+ handleNewPolygon("object_mask");
+ }}
+ >
+
+
+
+ Add Object Mask
+
+
+ {allPolygons
+ .flatMap((polygon, index) =>
+ polygon.type === "object_mask"
+ ? [{ polygon, index }]
+ : [],
+ )
+ .map(({ polygon, index }) => (
+
+ ))}
+
+ )}
- {(selectedZoneMask === undefined ||
- selectedZoneMask.includes("motion_mask" as PolygonType)) && (
- <>
-
-
Motion Masks
-
{
- setEditPane("motion_mask");
- handleNewPolygon("motion_mask");
- }}
- >
-
-
-
- {allPolygons
- .flatMap((polygon, index) =>
- polygon.type === "motion_mask"
- ? [{ polygon, index }]
- : [],
- )
- .map(({ polygon, index }) => (
-
- ))}
- >
- )}
-
-
-
- {(selectedZoneMask === undefined ||
- selectedZoneMask.includes("object_mask" as PolygonType)) && (
- <>
-
-
Object Masks
-
{
- setEditPane("object_mask");
- handleNewPolygon("object_mask");
- }}
- >
-
-
-
- {allPolygons
- .flatMap((polygon, index) =>
- polygon.type === "object_mask"
- ? [{ polygon, index }]
- : [],
- )
- .map(({ polygon, index }) => (
-
- ))}
- >
- )}
>
)}
{/*
@@ -661,7 +698,7 @@ export default function MasksAndZones({
ref={containerRef}
className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full"
>
-
+
{cameraConfig &&
scaledWidth &&
scaledHeight &&
@@ -677,7 +714,7 @@ export default function MasksAndZones({
selectedZoneMask={selectedZoneMask}
/>
) : (
-
+
)}
@@ -725,7 +762,7 @@ function PolygonItem({
{isMobile && <>>}
-
+
+
+
+
+ Edit
+
handleCopyCoordinates(index)}
>
-
+
+
+
+
+ Copy coordinates
+
setDeleteDialogOpen(true)}
>
-
+
+
+
+
+ Delete
+
)}
diff --git a/web/src/components/settings/MotionTuner.tsx b/web/src/components/settings/MotionTuner.tsx
index e1af979cb..d7af8aac6 100644
--- a/web/src/components/settings/MotionTuner.tsx
+++ b/web/src/components/settings/MotionTuner.tsx
@@ -1,13 +1,4 @@
import Heading from "@/components/ui/heading";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
@@ -37,13 +28,17 @@ import { Switch } from "../ui/switch";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
+type MotionTunerProps = {
+ selectedCamera: string;
+};
+
type MotionSettings = {
threshold?: number;
contour_area?: number;
improve_contrast?: boolean;
};
-export default function MotionTuner() {
+export default function MotionTuner({ selectedCamera }: MotionTunerProps) {
const { data: config, mutate: updateConfig } =
useSWR("config");
const [changedValue, setChangedValue] = useState(false);
@@ -60,7 +55,7 @@ export default function MotionTuner() {
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);
- const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
+ // const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
const [nextSelectedCamera, setNextSelectedCamera] = useState("");
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
@@ -169,11 +164,11 @@ export default function MotionTuner() {
setNextSelectedCamera(camera);
setConfirmationDialogOpen(true);
} else {
- setSelectedCamera(camera);
+ // setSelectedCamera(camera);
setNextSelectedCamera("");
}
},
- [setSelectedCamera, changedValue],
+ [changedValue],
);
const handleDialog = useCallback(
@@ -181,12 +176,12 @@ export default function MotionTuner() {
if (save) {
saveToConfig();
}
- setSelectedCamera(nextSelectedCamera);
+ // setSelectedCamera(nextSelectedCamera);
setNextSelectedCamera("");
setConfirmationDialogOpen(false);
setChangedValue(false);
},
- [saveToConfig, setSelectedCamera, nextSelectedCamera],
+ [saveToConfig],
);
if (!cameraConfig && !selectedCamera) {
@@ -194,127 +189,113 @@ export default function MotionTuner() {
}
return (
- <>
- Motion Detection Tuner
-
-
-
-
-
-
-
-
- Choose a camera
- {cameras.map((camera) => (
-
- {camera.name}
-
- ))}
-
-
-
-
- {cameraConfig ? (
-
-
-
-
- {
- handleMotionConfigChange({ threshold: value[0] });
- }}
- />
-
- Threshold: {motionSettings.threshold}
-
-
-
- {
- handleMotionConfigChange({ contour_area: value[0] });
- }}
- />
-
- Contour Area: {motionSettings.contour_area}
-
-
-
- {
- handleMotionConfigChange({ improve_contrast: isChecked });
- }}
- />
- Improve Contrast
-
+
+
+
+
+ Motion Detection Tuner
+
-
-
- {isLoading ? "Saving..." : "Save to Config"}
-
-
+
+
+ {
+ handleMotionConfigChange({ threshold: value[0] });
+ }}
+ />
+
+ Threshold: {motionSettings.threshold}
+
- {confirmationDialogOpen && (
-
setConfirmationDialogOpen(false)}
+
+ {
+ handleMotionConfigChange({ contour_area: value[0] });
+ }}
+ />
+
+ Contour Area: {motionSettings.contour_area}
+
+
+
+ {
+ handleMotionConfigChange({ improve_contrast: isChecked });
+ }}
+ />
+ Improve Contrast
+
+
+
+
-
-
-
- You have unsaved changes on this camera.
-
-
- Do you want to save your changes before continuing?
-
-
-
- handleDialog(false)}>
- Cancel
-
- handleDialog(true)}>
- Save
-
-
-
-
- )}
+ {isLoading ? "Saving..." : "Save to Config"}
+
+
+
+ {confirmationDialogOpen && (
+
setConfirmationDialogOpen(false)}
+ >
+
+
+
+ You have unsaved changes on this camera.
+
+
+ Do you want to save your changes before continuing?
+
+
+
+ handleDialog(false)}>
+ Cancel
+
+ handleDialog(true)}>
+ Save
+
+
+
+
+ )}
+
+
+ {cameraConfig ? (
+
) : (
)}
- >
+
);
}
diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx
index c29558e8f..f6d49a681 100644
--- a/web/src/components/settings/PolygonCanvas.tsx
+++ b/web/src/components/settings/PolygonCanvas.tsx
@@ -1,6 +1,6 @@
import React, { useMemo, useRef, useState, useEffect } from "react";
import PolygonDrawer from "./PolygonDrawer";
-import { Stage, Layer, Image, Text } from "react-konva";
+import { Stage, Layer, Image, Text, Circle } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon, PolygonType } from "@/types/canvas";
@@ -216,23 +216,43 @@ export function PolygonCanvas({
isHovered={index === hoveredPolygonIndex}
isFinished={polygon.isFinished}
color={polygon.color}
+ name={polygon.name}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
/>
{index === hoveredPolygonIndex && (
-
+ <>
+
+
+ >
)}
),
diff --git a/web/src/components/settings/PolygonDrawer.tsx b/web/src/components/settings/PolygonDrawer.tsx
index 708dbbc22..7a9294cf3 100644
--- a/web/src/components/settings/PolygonDrawer.tsx
+++ b/web/src/components/settings/PolygonDrawer.tsx
@@ -1,6 +1,11 @@
-import { useCallback, useState } from "react";
-import { Line, Circle, Group } from "react-konva";
-import { minMax, toRGBColorString, dragBoundFunc } from "@/utils/canvasUtil";
+import { useCallback, useRef, useState } from "react";
+import { Line, Circle, Group, Text } from "react-konva";
+import {
+ minMax,
+ toRGBColorString,
+ dragBoundFunc,
+ getAveragePoint,
+} from "@/utils/canvasUtil";
import type { KonvaEventObject } from "konva/lib/Node";
import Konva from "konva";
import { Vector2d } from "konva/lib/types";
@@ -12,6 +17,7 @@ type PolygonDrawerProps = {
isHovered: boolean;
isFinished: boolean;
color: number[];
+ name: string;
handlePointDragMove: (e: KonvaEventObject
) => void;
handleGroupDragEnd: (e: KonvaEventObject) => void;
handleMouseOverStartPoint: (
@@ -28,6 +34,7 @@ export default function PolygonDrawer({
isActive,
isHovered,
isFinished,
+ name,
color,
handlePointDragMove,
handleGroupDragEnd,
@@ -38,6 +45,7 @@ export default function PolygonDrawer({
const [stage, setStage] = useState();
const [minMaxX, setMinMaxX] = useState([0, 0]);
const [minMaxY, setMinMaxY] = useState([0, 0]);
+ const groupRef = useRef(null);
const handleGroupMouseOver = (
e: Konva.KonvaEventObject,
@@ -85,9 +93,12 @@ export default function PolygonDrawer({
[color],
);
+ // console.log(groupRef.current?.height());
+
return (
);
})}
+ {groupRef.current && (
+
+ )}
);
}
diff --git a/web/src/components/settings/PolygonEditControls.tsx b/web/src/components/settings/PolygonEditControls.tsx
index ed707aa74..88a859737 100644
--- a/web/src/components/settings/PolygonEditControls.tsx
+++ b/web/src/components/settings/PolygonEditControls.tsx
@@ -1,10 +1,12 @@
import { Polygon } from "@/types/canvas";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
+import { MdOutlineRestartAlt, MdUndo } from "react-icons/md";
import { Button } from "../ui/button";
type PolygonEditControlsProps = {
polygons: Polygon[];
setPolygons: React.Dispatch>;
- activePolygonIndex: number | null;
+ activePolygonIndex: number | undefined;
};
export default function PolygonEditControls({
@@ -13,39 +15,61 @@ export default function PolygonEditControls({
activePolygonIndex,
}: PolygonEditControlsProps) {
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);
- }
+ if (activePolygonIndex === undefined || !polygons) {
+ return;
}
+
+ const updatedPolygons = [...polygons];
+ const activePolygon = updatedPolygons[activePolygonIndex];
+ updatedPolygons[activePolygonIndex] = {
+ ...activePolygon,
+ points: [...activePolygon.points.slice(0, -1)],
+ isFinished: false,
+ };
+ setPolygons(updatedPolygons);
};
const reset = () => {
- if (activePolygonIndex !== null) {
- const updatedPolygons = [...polygons];
- updatedPolygons[activePolygonIndex] = {
- ...updatedPolygons[activePolygonIndex],
- points: [],
- };
- setPolygons(updatedPolygons);
+ if (activePolygonIndex === undefined || !polygons) {
+ return;
}
+
+ const updatedPolygons = [...polygons];
+ const activePolygon = updatedPolygons[activePolygonIndex];
+ updatedPolygons[activePolygonIndex] = {
+ ...activePolygon,
+ points: [],
+ isFinished: false,
+ };
+ setPolygons(updatedPolygons);
};
return (
-
-
- Undo
-
-
- Reset
-
+
+
+
+
+
+
+
+ Undo
+
+
+
+
+
+
+
+ Reset
+
);
}
diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx
index 23167701a..03342ea4c 100644
--- a/web/src/components/settings/ZoneEditPane.tsx
+++ b/web/src/components/settings/ZoneEditPane.tsx
@@ -21,9 +21,11 @@ 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;
@@ -31,6 +33,7 @@ type ZoneEditPaneProps = {
export function ZoneEditPane({
polygons,
+ setPolygons,
activePolygonIndex,
onSave,
onCancel,
@@ -133,10 +136,26 @@ export function ZoneEditPane({
Zone
-
-
+
+ {polygons && activePolygonIndex !== undefined && (
+
+
+ {polygons[activePolygonIndex].points.length} points
+
+ {polygons[activePolygonIndex].isFinished ? <>> : <>>}
+
+
+ )}
+
+ Click to draw a polygon on the image.
+
+