filtering

This commit is contained in:
Josh Hawkins 2024-04-13 07:48:31 -05:00
parent 52af3cef9b
commit 72e7e67b29
5 changed files with 367 additions and 185 deletions

View File

@ -0,0 +1,130 @@
import { Button } from "../ui/button";
import { FaFilter } from "react-icons/fa";
import { isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { PolygonType } from "@/types/canvas";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
type ZoneMaskFilterButtonProps = {
selectedZoneMask?: PolygonType[];
updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void;
};
export function ZoneMaskFilterButton({
selectedZoneMask,
updateZoneMaskFilter,
}: ZoneMaskFilterButtonProps) {
const trigger = (
<Button size="sm" className="flex items-center gap-2">
<FaFilter className="text-secondary-foreground" />
<div className="hidden md:block text-primary">Filter</div>
</Button>
);
const content = (
<GeneralFilterContent
selectedZoneMask={selectedZoneMask}
updateZoneMaskFilter={updateZoneMaskFilter}
/>
);
if (isMobile) {
return (
<Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] p-3 mx-1 overflow-hidden">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<Popover>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent>
</Popover>
);
}
type GeneralFilterContentProps = {
selectedZoneMask: PolygonType[] | undefined;
updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void;
};
export function GeneralFilterContent({
selectedZoneMask,
updateZoneMaskFilter,
}: GeneralFilterContentProps) {
return (
<>
<div className="h-auto overflow-y-auto overflow-x-hidden">
<div className="flex justify-between items-center my-2.5">
<Label
className="mx-2 text-primary cursor-pointer"
htmlFor="allLabels"
>
All Masks and Zones
</Label>
<Switch
className="ml-1"
id="allLabels"
checked={selectedZoneMask == undefined}
onCheckedChange={(isChecked) => {
if (isChecked) {
updateZoneMaskFilter(undefined);
}
}}
/>
</div>
<DropdownMenuSeparator />
<div className="my-2.5 flex flex-col gap-2.5">
{["zone", "motion_mask", "object_mask"].map((item) => (
<div className="flex justify-between items-center">
<Label
className="w-full mx-2 text-primary capitalize cursor-pointer"
htmlFor={item}
>
{item
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase()) + "s"}
</Label>
<Switch
key={item}
className="ml-1"
id={item}
checked={
selectedZoneMask?.includes(item as PolygonType) ?? false
}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedLabels = selectedZoneMask
? [...selectedZoneMask]
: [];
updatedLabels.push(item as PolygonType);
updateZoneMaskFilter(updatedLabels);
} else {
const updatedLabels = selectedZoneMask
? [...selectedZoneMask]
: [];
// can not deselect the last item
if (updatedLabels.length > 1) {
updatedLabels.splice(
updatedLabels.indexOf(item as PolygonType),
1,
);
updateZoneMaskFilter(updatedLabels);
}
}
}}
/>
</div>
))}
</div>
</div>
<DropdownMenuSeparator />
</>
);
}

View File

@ -180,9 +180,13 @@ export type ZoneObjects = {
type MasksAndZoneProps = {
selectedCamera: string;
selectedZoneMask: PolygonType;
};
export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
export default function MasksAndZones({
selectedCamera,
selectedZoneMask,
}: MasksAndZoneProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const [allPolygons, setAllPolygons] = useState<Polygon[]>([]);
const [editingPolygons, setEditingPolygons] = useState<Polygon[]>();
@ -266,23 +270,6 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
[setZoneObjects],
);
// 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;
// },
// [config],
// );
const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef);
@ -322,7 +309,6 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
const scaledHeight = useMemo(() => {
if (containerRef.current && aspectRatio && detectHeight) {
console.log("recalc", Date.now());
const scaledHeight =
aspectRatio < (fitAspect ?? 0)
? Math.floor(
@ -405,9 +391,9 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
useEffect(() => {
if (cameraConfig && containerRef.current && scaledWidth) {
setAllPolygons([
...Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
type: "zone" as PolygonType, // Add the type property here
const zones = Object.entries(cameraConfig.zones).map(
([name, zoneData]) => ({
type: "zone" as PolygonType,
camera: cameraConfig.name,
name,
points: interpolatePoints(
@ -419,11 +405,14 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
),
isFinished: true,
color: zoneData.color,
})),
...Object.entries(cameraConfig.motion.mask).map(([, maskData]) => ({
}),
);
const motionMasks = Object.entries(cameraConfig.motion.mask).map(
([, maskData], index) => ({
type: "motion_mask" as PolygonType,
camera: cameraConfig.name,
name: "motion_mask",
name: `Motion Mask ${index + 1}`,
points: interpolatePoints(
parseCoordinates(maskData),
1,
@ -433,32 +422,59 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
),
isFinished: true,
color: [0, 0, 255],
})),
...Object.entries(cameraConfig.objects.filters).flatMap(
([objectName, { mask }]): Polygon[] =>
mask !== null && mask !== undefined
? mask.flatMap((maskItem) =>
maskItem !== null && maskItem !== undefined
? [
{
type: "object_mask" as PolygonType,
camera: cameraConfig.name,
name: objectName,
points: interpolatePoints(
parseCoordinates(maskItem),
1,
1,
scaledWidth,
scaledHeight,
),
isFinished: true,
color: [128, 128, 128],
},
]
: [],
)
: [],
),
}),
);
const globalObjectMasks = Object.entries(cameraConfig.objects.mask).map(
([, maskData], index) => ({
type: "object_mask" as PolygonType,
camera: cameraConfig.name,
name: `All Objects Object Mask ${index + 1}`,
points: interpolatePoints(
parseCoordinates(maskData),
1,
1,
scaledWidth,
scaledHeight,
),
isFinished: true,
color: [0, 0, 255],
}),
);
const globalObjectMasksCount = globalObjectMasks.length;
const objectMasks = Object.entries(cameraConfig.objects.filters).flatMap(
([objectName, { mask }]): Polygon[] =>
mask !== null && mask !== undefined
? mask.flatMap((maskItem, subIndex) =>
maskItem !== null && maskItem !== undefined
? [
{
type: "object_mask" as PolygonType,
camera: cameraConfig.name,
name: `${objectName.charAt(0).toUpperCase() + objectName.slice(1)} Object Mask ${globalObjectMasksCount + subIndex + 1}`,
points: interpolatePoints(
parseCoordinates(maskItem),
1,
1,
scaledWidth,
scaledHeight,
),
isFinished: true,
color: [128, 128, 128],
},
]
: [],
)
: [],
);
setAllPolygons([
...zones,
...motionMasks,
...globalObjectMasks,
...objectMasks,
]);
setZoneObjects(
@ -502,12 +518,14 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
return <ActivityIndicator />;
}
// console.log(selectedZoneMask);
return (
<>
{cameraConfig && allPolygons && (
<div className="flex flex-col md:flex-row size-full">
<Toaster position="top-center" />
<div className="flex flex-col order-last w-full overflow-y-auto md:w-3/12 md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
<div className="flex flex-col w-full overflow-y-auto md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
{editPane == "zone" && (
<ZoneEditPane
polygons={allPolygons}
@ -530,107 +548,126 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
onCancel={handleCancel}
/>
)}
{editPane == undefined && (
{editPane === undefined && (
<>
<div className="flex flex-row justify-between items-center mb-3">
<div className="text-md">Zones</div>
<Button
variant="ghost"
className="h-8 px-0"
onClick={() => {
setEditPane("zone");
handleNewPolygon("zone");
}}
>
<LuPlusSquare />
</Button>
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "zone" ? [{ polygon, index }] : [],
)
.map(({ polygon, index }) => (
<PolygonItem
key={index}
polygon={polygon}
index={index}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
setHoveredPolygonIndex={setHoveredPolygonIndex}
deleteDialogOpen={deleteDialogOpen}
setDeleteDialogOpen={setDeleteDialogOpen}
setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane}
setAllPolygons={setAllPolygons}
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Motion Masks</div>
<Button
variant="ghost"
className="h-8 px-0"
onClick={() => {
setEditPane("motion_mask");
handleNewPolygon("motion_mask");
}}
>
<LuPlusSquare />
</Button>
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "motion_mask" ? [{ polygon, index }] : [],
)
.map(({ polygon, index }) => (
<PolygonItem
key={index}
polygon={polygon}
index={index}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
setHoveredPolygonIndex={setHoveredPolygonIndex}
deleteDialogOpen={deleteDialogOpen}
setDeleteDialogOpen={setDeleteDialogOpen}
setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane}
setAllPolygons={setAllPolygons}
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Object Masks</div>
<Button
variant="ghost"
className="h-8 px-0"
onClick={() => {
setEditPane("motion_mask");
handleNewPolygon("motion_mask");
}}
>
<LuPlusSquare />
</Button>
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "object_mask" ? [{ polygon, index }] : [],
)
.map(({ polygon, index }) => (
<PolygonItem
key={index}
polygon={polygon}
index={index}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
setHoveredPolygonIndex={setHoveredPolygonIndex}
deleteDialogOpen={deleteDialogOpen}
setDeleteDialogOpen={setDeleteDialogOpen}
setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane}
setAllPolygons={setAllPolygons}
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("zone" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center mb-3">
<div className="text-md">Zones</div>
<Button
variant="ghost"
className="h-8 px-0"
onClick={() => {
setEditPane("zone");
handleNewPolygon("zone");
}}
>
<LuPlusSquare />
</Button>
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "zone" ? [{ polygon, index }] : [],
)
.map(({ polygon, index }) => (
<PolygonItem
key={index}
polygon={polygon}
index={index}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
setHoveredPolygonIndex={setHoveredPolygonIndex}
deleteDialogOpen={deleteDialogOpen}
setDeleteDialogOpen={setDeleteDialogOpen}
setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane}
setAllPolygons={setAllPolygons}
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
</>
)}
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("motion_mask" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Motion Masks</div>
<Button
variant="ghost"
className="h-8 px-0"
onClick={() => {
setEditPane("motion_mask");
handleNewPolygon("motion_mask");
}}
>
<LuPlusSquare />
</Button>
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "motion_mask"
? [{ polygon, index }]
: [],
)
.map(({ polygon, index }) => (
<PolygonItem
key={index}
polygon={polygon}
index={index}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
setHoveredPolygonIndex={setHoveredPolygonIndex}
deleteDialogOpen={deleteDialogOpen}
setDeleteDialogOpen={setDeleteDialogOpen}
setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane}
setAllPolygons={setAllPolygons}
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
</>
)}
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("object_mask" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Object Masks</div>
<Button
variant="ghost"
className="h-8 px-0"
onClick={() => {
setEditPane("motion_mask");
handleNewPolygon("motion_mask");
}}
>
<LuPlusSquare />
</Button>
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "object_mask"
? [{ polygon, index }]
: [],
)
.map(({ polygon, index }) => (
<PolygonItem
key={index}
polygon={polygon}
index={index}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
setHoveredPolygonIndex={setHoveredPolygonIndex}
deleteDialogOpen={deleteDialogOpen}
setDeleteDialogOpen={setDeleteDialogOpen}
setActivePolygonIndex={setActivePolygonIndex}
setEditPane={setEditPane}
setAllPolygons={setAllPolygons}
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
</>
)}
</>
)}
{/* <Table>
@ -728,6 +765,7 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
setPolygons={setEditingPolygons}
activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex}
selectedZoneMask={selectedZoneMask}
/>
) : (
<Skeleton className="w-full h-full" />

View File

@ -3,7 +3,7 @@ import PolygonDrawer from "./PolygonDrawer";
import { Stage, Layer, Image, Text } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon } from "@/types/canvas";
import { Polygon, PolygonType } from "@/types/canvas";
import { useApiHost } from "@/api";
import { getAveragePoint } from "@/utils/canvasUtil";
@ -16,6 +16,7 @@ type PolygonCanvasProps = {
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | undefined;
hoveredPolygonIndex: number | null;
selectedZoneMask: PolygonType;
};
export function PolygonCanvas({
@ -27,6 +28,7 @@ export function PolygonCanvas({
setPolygons,
activePolygonIndex,
hoveredPolygonIndex,
selectedZoneMask,
}: PolygonCanvasProps) {
const [image, setImage] = useState<HTMLImageElement | undefined>();
const imageRef = useRef<Konva.Image | null>(null);
@ -205,36 +207,40 @@ export function PolygonCanvas({
width={width}
height={height}
/>
{polygons?.map((polygon, index) => (
<React.Fragment key={index}>
<PolygonDrawer
key={index}
points={polygon.points}
flattenedPoints={flattenPoints(polygon.points)}
isActive={index === activePolygonIndex}
isHovered={index === hoveredPolygonIndex}
isFinished={polygon.isFinished}
color={polygon.color}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
/>
{index === hoveredPolygonIndex && (
<Text
text={polygon.name}
align="left"
verticalAlign="top"
x={
getAveragePoint(flattenPoints(polygon.points)).x
// - (polygon.name.length * 16 * 0.6) / 2
}
y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2}
fontSize={16}
/>
)}
</React.Fragment>
))}
{polygons?.map(
(polygon, index) =>
(selectedZoneMask === undefined ||
selectedZoneMask.includes(polygon.type)) && (
<React.Fragment key={index}>
<PolygonDrawer
key={index}
points={polygon.points}
flattenedPoints={flattenPoints(polygon.points)}
isActive={index === activePolygonIndex}
isHovered={index === hoveredPolygonIndex}
isFinished={polygon.isFinished}
color={polygon.color}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
/>
{index === hoveredPolygonIndex && (
<Text
text={polygon.name}
align="left"
verticalAlign="top"
x={
getAveragePoint(flattenPoints(polygon.points)).x
// - (polygon.name.length * 16 * 0.6) / 2
}
y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2}
fontSize={16}
/>
)}
</React.Fragment>
),
)}
</Layer>
</Stage>
);

View File

@ -20,6 +20,8 @@ import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import General from "@/components/settings/General";
import FilterCheckBox from "@/components/filter/FilterCheckBox";
import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter";
import { PolygonType } from "@/types/canvas";
type CameraSelectButtonProps = {
allCameras: CameraConfig[];
@ -136,6 +138,8 @@ export default function Settings() {
const [selectedCamera, setSelectedCamera] = useState(cameras[0].name);
const [filterZoneMask, setFilterZoneMask] = useState<PolygonType[]>();
return (
<div className="size-full p-2 flex flex-col">
<div className="w-full h-11 relative flex justify-between items-center">
@ -168,6 +172,10 @@ export default function Settings() {
page == "masks / zones" ||
page == "motion tuner") && (
<div className="flex items-center gap-2">
<ZoneMaskFilterButton
selectedZoneMask={filterZoneMask}
updateZoneMaskFilter={setFilterZoneMask}
/>
<CameraSelectButton
allCameras={cameras}
selectedCamera={selectedCamera}
@ -182,7 +190,7 @@ export default function Settings() {
{page == "masks / zones" && (
<MasksAndZones
selectedCamera={selectedCamera}
setSelectedCamera={setSelectedCamera}
selectedZoneMask={filterZoneMask}
/>
)}
{page == "motion tuner" && <MotionTuner />}

View File

@ -340,7 +340,7 @@ export interface FrigateConfig {
threshold: number;
};
};
mask: string;
mask: string[];
track: string[];
};