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 = { type MasksAndZoneProps = {
selectedCamera: string; selectedCamera: string;
selectedZoneMask: PolygonType;
}; };
export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) { export default function MasksAndZones({
selectedCamera,
selectedZoneMask,
}: MasksAndZoneProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [allPolygons, setAllPolygons] = useState<Polygon[]>([]); const [allPolygons, setAllPolygons] = useState<Polygon[]>([]);
const [editingPolygons, setEditingPolygons] = useState<Polygon[]>(); const [editingPolygons, setEditingPolygons] = useState<Polygon[]>();
@ -266,23 +270,6 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
[setZoneObjects], [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 }] = const [{ width: containerWidth, height: containerHeight }] =
useResizeObserver(containerRef); useResizeObserver(containerRef);
@ -322,7 +309,6 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
const scaledHeight = useMemo(() => { const scaledHeight = useMemo(() => {
if (containerRef.current && aspectRatio && detectHeight) { if (containerRef.current && aspectRatio && detectHeight) {
console.log("recalc", Date.now());
const scaledHeight = const scaledHeight =
aspectRatio < (fitAspect ?? 0) aspectRatio < (fitAspect ?? 0)
? Math.floor( ? Math.floor(
@ -405,9 +391,9 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
useEffect(() => { useEffect(() => {
if (cameraConfig && containerRef.current && scaledWidth) { if (cameraConfig && containerRef.current && scaledWidth) {
setAllPolygons([ const zones = Object.entries(cameraConfig.zones).map(
...Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({ ([name, zoneData]) => ({
type: "zone" as PolygonType, // Add the type property here type: "zone" as PolygonType,
camera: cameraConfig.name, camera: cameraConfig.name,
name, name,
points: interpolatePoints( points: interpolatePoints(
@ -419,11 +405,14 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
), ),
isFinished: true, isFinished: true,
color: zoneData.color, 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, type: "motion_mask" as PolygonType,
camera: cameraConfig.name, camera: cameraConfig.name,
name: "motion_mask", name: `Motion Mask ${index + 1}`,
points: interpolatePoints( points: interpolatePoints(
parseCoordinates(maskData), parseCoordinates(maskData),
1, 1,
@ -433,32 +422,59 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
), ),
isFinished: true, isFinished: true,
color: [0, 0, 255], color: [0, 0, 255],
})), }),
...Object.entries(cameraConfig.objects.filters).flatMap( );
([objectName, { mask }]): Polygon[] =>
mask !== null && mask !== undefined const globalObjectMasks = Object.entries(cameraConfig.objects.mask).map(
? mask.flatMap((maskItem) => ([, maskData], index) => ({
maskItem !== null && maskItem !== undefined type: "object_mask" as PolygonType,
? [ camera: cameraConfig.name,
{ name: `All Objects Object Mask ${index + 1}`,
type: "object_mask" as PolygonType, points: interpolatePoints(
camera: cameraConfig.name, parseCoordinates(maskData),
name: objectName, 1,
points: interpolatePoints( 1,
parseCoordinates(maskItem), scaledWidth,
1, scaledHeight,
1, ),
scaledWidth, isFinished: true,
scaledHeight, color: [0, 0, 255],
), }),
isFinished: true, );
color: [128, 128, 128],
}, 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( setZoneObjects(
@ -502,12 +518,14 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
// console.log(selectedZoneMask);
return ( return (
<> <>
{cameraConfig && allPolygons && ( {cameraConfig && allPolygons && (
<div className="flex flex-col md:flex-row size-full"> <div className="flex flex-col md:flex-row size-full">
<Toaster position="top-center" /> <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" && ( {editPane == "zone" && (
<ZoneEditPane <ZoneEditPane
polygons={allPolygons} polygons={allPolygons}
@ -530,107 +548,126 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
onCancel={handleCancel} onCancel={handleCancel}
/> />
)} )}
{editPane == undefined && ( {editPane === undefined && (
<> <>
<div className="flex flex-row justify-between items-center mb-3"> {(selectedZoneMask === undefined ||
<div className="text-md">Zones</div> selectedZoneMask.includes("zone" as PolygonType)) && (
<Button <>
variant="ghost" <div className="flex flex-row justify-between items-center mb-3">
className="h-8 px-0" <div className="text-md">Zones</div>
onClick={() => { <Button
setEditPane("zone"); variant="ghost"
handleNewPolygon("zone"); className="h-8 px-0"
}} onClick={() => {
> setEditPane("zone");
<LuPlusSquare /> handleNewPolygon("zone");
</Button> }}
</div> >
{allPolygons <LuPlusSquare />
.flatMap((polygon, index) => </Button>
polygon.type === "zone" ? [{ polygon, index }] : [], </div>
) {allPolygons
.map(({ polygon, index }) => ( .flatMap((polygon, index) =>
<PolygonItem polygon.type === "zone" ? [{ polygon, index }] : [],
key={index} )
polygon={polygon} .map(({ polygon, index }) => (
index={index} <PolygonItem
activePolygonIndex={activePolygonIndex} key={index}
hoveredPolygonIndex={hoveredPolygonIndex} polygon={polygon}
setHoveredPolygonIndex={setHoveredPolygonIndex} index={index}
deleteDialogOpen={deleteDialogOpen} activePolygonIndex={activePolygonIndex}
setDeleteDialogOpen={setDeleteDialogOpen} hoveredPolygonIndex={hoveredPolygonIndex}
setActivePolygonIndex={setActivePolygonIndex} setHoveredPolygonIndex={setHoveredPolygonIndex}
setEditPane={setEditPane} deleteDialogOpen={deleteDialogOpen}
setAllPolygons={setAllPolygons} setDeleteDialogOpen={setDeleteDialogOpen}
handleCopyCoordinates={handleCopyCoordinates} setActivePolygonIndex={setActivePolygonIndex}
/> setEditPane={setEditPane}
))} setAllPolygons={setAllPolygons}
<div className="flex flex-row justify-between items-center my-3"> handleCopyCoordinates={handleCopyCoordinates}
<div className="text-md">Motion Masks</div> />
<Button ))}
variant="ghost" </>
className="h-8 px-0" )}
onClick={() => { {(selectedZoneMask === undefined ||
setEditPane("motion_mask"); selectedZoneMask.includes("motion_mask" as PolygonType)) && (
handleNewPolygon("motion_mask"); <>
}} <div className="flex flex-row justify-between items-center my-3">
> <div className="text-md">Motion Masks</div>
<LuPlusSquare /> <Button
</Button> variant="ghost"
</div> className="h-8 px-0"
{allPolygons onClick={() => {
.flatMap((polygon, index) => setEditPane("motion_mask");
polygon.type === "motion_mask" ? [{ polygon, index }] : [], handleNewPolygon("motion_mask");
) }}
.map(({ polygon, index }) => ( >
<PolygonItem <LuPlusSquare />
key={index} </Button>
polygon={polygon} </div>
index={index} {allPolygons
activePolygonIndex={activePolygonIndex} .flatMap((polygon, index) =>
hoveredPolygonIndex={hoveredPolygonIndex} polygon.type === "motion_mask"
setHoveredPolygonIndex={setHoveredPolygonIndex} ? [{ polygon, index }]
deleteDialogOpen={deleteDialogOpen} : [],
setDeleteDialogOpen={setDeleteDialogOpen} )
setActivePolygonIndex={setActivePolygonIndex} .map(({ polygon, index }) => (
setEditPane={setEditPane} <PolygonItem
setAllPolygons={setAllPolygons} key={index}
handleCopyCoordinates={handleCopyCoordinates} polygon={polygon}
/> index={index}
))} activePolygonIndex={activePolygonIndex}
<div className="flex flex-row justify-between items-center my-3"> hoveredPolygonIndex={hoveredPolygonIndex}
<div className="text-md">Object Masks</div> setHoveredPolygonIndex={setHoveredPolygonIndex}
<Button deleteDialogOpen={deleteDialogOpen}
variant="ghost" setDeleteDialogOpen={setDeleteDialogOpen}
className="h-8 px-0" setActivePolygonIndex={setActivePolygonIndex}
onClick={() => { setEditPane={setEditPane}
setEditPane("motion_mask"); setAllPolygons={setAllPolygons}
handleNewPolygon("motion_mask"); handleCopyCoordinates={handleCopyCoordinates}
}} />
> ))}
<LuPlusSquare /> </>
</Button> )}
</div> {(selectedZoneMask === undefined ||
{allPolygons selectedZoneMask.includes("object_mask" as PolygonType)) && (
.flatMap((polygon, index) => <>
polygon.type === "object_mask" ? [{ polygon, index }] : [], <div className="flex flex-row justify-between items-center my-3">
) <div className="text-md">Object Masks</div>
.map(({ polygon, index }) => ( <Button
<PolygonItem variant="ghost"
key={index} className="h-8 px-0"
polygon={polygon} onClick={() => {
index={index} setEditPane("motion_mask");
activePolygonIndex={activePolygonIndex} handleNewPolygon("motion_mask");
hoveredPolygonIndex={hoveredPolygonIndex} }}
setHoveredPolygonIndex={setHoveredPolygonIndex} >
deleteDialogOpen={deleteDialogOpen} <LuPlusSquare />
setDeleteDialogOpen={setDeleteDialogOpen} </Button>
setActivePolygonIndex={setActivePolygonIndex} </div>
setEditPane={setEditPane} {allPolygons
setAllPolygons={setAllPolygons} .flatMap((polygon, index) =>
handleCopyCoordinates={handleCopyCoordinates} 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> {/* <Table>
@ -728,6 +765,7 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
setPolygons={setEditingPolygons} setPolygons={setEditingPolygons}
activePolygonIndex={activePolygonIndex} activePolygonIndex={activePolygonIndex}
hoveredPolygonIndex={hoveredPolygonIndex} hoveredPolygonIndex={hoveredPolygonIndex}
selectedZoneMask={selectedZoneMask}
/> />
) : ( ) : (
<Skeleton className="w-full h-full" /> <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 { Stage, Layer, Image, Text } from "react-konva";
import Konva from "konva"; import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node"; import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon } from "@/types/canvas"; import { Polygon, PolygonType } from "@/types/canvas";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import { getAveragePoint } from "@/utils/canvasUtil"; import { getAveragePoint } from "@/utils/canvasUtil";
@ -16,6 +16,7 @@ type PolygonCanvasProps = {
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>; setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | undefined; activePolygonIndex: number | undefined;
hoveredPolygonIndex: number | null; hoveredPolygonIndex: number | null;
selectedZoneMask: PolygonType;
}; };
export function PolygonCanvas({ export function PolygonCanvas({
@ -27,6 +28,7 @@ export function PolygonCanvas({
setPolygons, setPolygons,
activePolygonIndex, activePolygonIndex,
hoveredPolygonIndex, hoveredPolygonIndex,
selectedZoneMask,
}: PolygonCanvasProps) { }: PolygonCanvasProps) {
const [image, setImage] = useState<HTMLImageElement | undefined>(); const [image, setImage] = useState<HTMLImageElement | undefined>();
const imageRef = useRef<Konva.Image | null>(null); const imageRef = useRef<Konva.Image | null>(null);
@ -205,36 +207,40 @@ export function PolygonCanvas({
width={width} width={width}
height={height} height={height}
/> />
{polygons?.map((polygon, index) => ( {polygons?.map(
<React.Fragment key={index}> (polygon, index) =>
<PolygonDrawer (selectedZoneMask === undefined ||
key={index} selectedZoneMask.includes(polygon.type)) && (
points={polygon.points} <React.Fragment key={index}>
flattenedPoints={flattenPoints(polygon.points)} <PolygonDrawer
isActive={index === activePolygonIndex} key={index}
isHovered={index === hoveredPolygonIndex} points={polygon.points}
isFinished={polygon.isFinished} flattenedPoints={flattenPoints(polygon.points)}
color={polygon.color} isActive={index === activePolygonIndex}
handlePointDragMove={handlePointDragMove} isHovered={index === hoveredPolygonIndex}
handleGroupDragEnd={handleGroupDragEnd} isFinished={polygon.isFinished}
handleMouseOverStartPoint={handleMouseOverStartPoint} color={polygon.color}
handleMouseOutStartPoint={handleMouseOutStartPoint} handlePointDragMove={handlePointDragMove}
/> handleGroupDragEnd={handleGroupDragEnd}
{index === hoveredPolygonIndex && ( handleMouseOverStartPoint={handleMouseOverStartPoint}
<Text handleMouseOutStartPoint={handleMouseOutStartPoint}
text={polygon.name} />
align="left" {index === hoveredPolygonIndex && (
verticalAlign="top" <Text
x={ text={polygon.name}
getAveragePoint(flattenPoints(polygon.points)).x align="left"
// - (polygon.name.length * 16 * 0.6) / 2 verticalAlign="top"
} x={
y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2} getAveragePoint(flattenPoints(polygon.points)).x
fontSize={16} // - (polygon.name.length * 16 * 0.6) / 2
/> }
)} y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2}
</React.Fragment> fontSize={16}
))} />
)}
</React.Fragment>
),
)}
</Layer> </Layer>
</Stage> </Stage>
); );

View File

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

View File

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