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,17 +422,38 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
),
isFinished: true,
color: [0, 0, 255],
})),
...Object.entries(cameraConfig.objects.filters).flatMap(
}),
);
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) =>
? mask.flatMap((maskItem, subIndex) =>
maskItem !== null && maskItem !== undefined
? [
{
type: "object_mask" as PolygonType,
camera: cameraConfig.name,
name: objectName,
name: `${objectName.charAt(0).toUpperCase() + objectName.slice(1)} Object Mask ${globalObjectMasksCount + subIndex + 1}`,
points: interpolatePoints(
parseCoordinates(maskItem),
1,
@ -458,7 +468,13 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
: [],
)
: [],
),
);
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,7 +548,10 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
onCancel={handleCancel}
/>
)}
{editPane == undefined && (
{editPane === undefined && (
<>
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("zone" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center mb-3">
<div className="text-md">Zones</div>
@ -565,6 +586,11 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
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
@ -580,7 +606,9 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "motion_mask" ? [{ polygon, index }] : [],
polygon.type === "motion_mask"
? [{ polygon, index }]
: [],
)
.map(({ polygon, index }) => (
<PolygonItem
@ -598,6 +626,11 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
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
@ -613,7 +646,9 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
</div>
{allPolygons
.flatMap((polygon, index) =>
polygon.type === "object_mask" ? [{ polygon, index }] : [],
polygon.type === "object_mask"
? [{ polygon, index }]
: [],
)
.map(({ polygon, index }) => (
<PolygonItem
@ -633,6 +668,8 @@ export default function MasksAndZones({ selectedCamera }: MasksAndZoneProps) {
))}
</>
)}
</>
)}
{/* <Table>
<TableHeader>
<TableRow>
@ -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,7 +207,10 @@ export function PolygonCanvas({
width={width}
height={height}
/>
{polygons?.map((polygon, index) => (
{polygons?.map(
(polygon, index) =>
(selectedZoneMask === undefined ||
selectedZoneMask.includes(polygon.type)) && (
<React.Fragment key={index}>
<PolygonDrawer
key={index}
@ -234,7 +239,8 @@ export function PolygonCanvas({
/>
)}
</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[];
};