From 0ff43d975df077da770cdd147b73f2b0aa33f382 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 10:30:32 +0000 Subject: [PATCH 1/6] - added digital zoom functionality on fixed location via web sockets --- src/app/config/wsconfig.ts | 7 +++- src/app/context/WebSocketContext.ts | 13 ++++++ src/app/providers/CameraFeedProvider.tsx | 1 - src/app/providers/WebSocketProvider.tsx | 34 ++++++++++++++- .../CameraSettings/RegionSelector.tsx | 42 ++++++++++++++++++- .../components/Video/VideoFeedGridPainter.tsx | 1 + src/utils/config.ts | 2 + 7 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/app/config/wsconfig.ts b/src/app/config/wsconfig.ts index d6fa33d..958ea01 100644 --- a/src/app/config/wsconfig.ts +++ b/src/app/config/wsconfig.ts @@ -1,5 +1,10 @@ +import { CAMBASE_WS } from "../../utils/config"; + export const wsConfig = { - infoBar: "ws://100.115.125.56/websocket-infobar", + infoBar: `${CAMBASE_WS}/websocket-infobar`, + cameraFeedA: `${CAMBASE_WS}/websocket-CameraA-live-video`, + cameraFeedB: `${CAMBASE_WS}/websocket-CameraB-live-video`, + cameraFeedC: `${CAMBASE_WS}/websocket-CameraC-live-video`, }; export type SocketKey = keyof typeof wsConfig; diff --git a/src/app/context/WebSocketContext.ts b/src/app/context/WebSocketContext.ts index 65799c0..dd89d90 100644 --- a/src/app/context/WebSocketContext.ts +++ b/src/app/context/WebSocketContext.ts @@ -6,10 +6,20 @@ type InfoSocketState = { data: InfoBarData | null; readyState: ReadyState; sendJson: (msg: unknown) => void; + send?: (msg: string) => void; +}; + +type CameraSocketState = { + data: null; + readyState: ReadyState; + send: (msg: string) => void; }; export type WebSocketConextValue = { info: InfoSocketState; + cameraFeedA: CameraSocketState; + cameraFeedB: CameraSocketState; + cameraFeedC: CameraSocketState; }; export const WebsocketContext = createContext(null); @@ -21,3 +31,6 @@ const useWebSocketContext = () => { }; export const useInfoSocket = () => useWebSocketContext().info; +export const useCameraFeedASocket = () => useWebSocketContext().cameraFeedA; +export const useCameraFeedBSocket = () => useWebSocketContext().cameraFeedB; +export const useCameraFeedCSocket = () => useWebSocketContext().cameraFeedC; diff --git a/src/app/providers/CameraFeedProvider.tsx b/src/app/providers/CameraFeedProvider.tsx index 8e818e2..5c0b39e 100644 --- a/src/app/providers/CameraFeedProvider.tsx +++ b/src/app/providers/CameraFeedProvider.tsx @@ -42,7 +42,6 @@ export const CameraFeedProvider = ({ children }: { children: ReactNode }) => { cameraZoomQueryC.refetch(), ]); - console.log(resultA?.data); const zoomLevelAnumber = parseFloat(resultA.data?.propPhysCurrent?.value); const zoomLevelBnumber = parseFloat(resultB.data?.propPhysCurrent?.value); const zoomLevelCnumber = parseFloat(resultC.data?.propPhysCurrent?.value); diff --git a/src/app/providers/WebSocketProvider.tsx b/src/app/providers/WebSocketProvider.tsx index 42ff194..d2346a2 100644 --- a/src/app/providers/WebSocketProvider.tsx +++ b/src/app/providers/WebSocketProvider.tsx @@ -10,7 +10,11 @@ type WebSocketProviderProps = { export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const [systemData, setSystemData] = useState(null); + const infoSocket = useWebSocket(wsConfig.infoBar, { share: true, shouldReconnect: () => true }); + const cameraFeedASocket = useWebSocket(wsConfig.cameraFeedA, { share: true, shouldReconnect: () => true }); + const cameraFeedBSocket = useWebSocket(wsConfig.cameraFeedB, { share: true, shouldReconnect: () => true }); + const cameraFeedCSocket = useWebSocket(wsConfig.cameraFeedC, { share: true, shouldReconnect: () => true }); useEffect(() => { async function parseData() { @@ -30,8 +34,36 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { readyState: infoSocket.readyState, sendJson: infoSocket.sendJsonMessage, }, + cameraFeedA: { + data: null, + readyState: cameraFeedASocket.readyState, + + send: cameraFeedASocket.sendMessage, + }, + cameraFeedB: { + data: null, + readyState: cameraFeedBSocket.readyState, + + send: cameraFeedBSocket.sendMessage, + }, + cameraFeedC: { + data: null, + readyState: cameraFeedCSocket.readyState, + + send: cameraFeedCSocket.sendMessage, + }, }), - [infoSocket.readyState, infoSocket.sendJsonMessage, systemData], + [ + cameraFeedASocket.readyState, + cameraFeedASocket.sendMessage, + cameraFeedBSocket.readyState, + cameraFeedBSocket.sendMessage, + cameraFeedCSocket.readyState, + cameraFeedCSocket.sendMessage, + infoSocket.readyState, + infoSocket.sendJsonMessage, + systemData, + ], ); return {children}; diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index f23722d..4b78604 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -4,6 +4,12 @@ import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext" import { useColourDectection } from "../../hooks/useColourDetection"; import { useBlackBoard } from "../../../../hooks/useBlackBoard"; import { toast } from "sonner"; +import { ReadyState } from "react-use-websocket"; +import { + useCameraFeedASocket, + useCameraFeedBSocket, + useCameraFeedCSocket, +} from "../../../../app/context/WebSocketContext"; type RegionSelectorProps = { regions: Region[]; @@ -29,6 +35,22 @@ const RegionSelector = ({ const { blackboardMutation } = useBlackBoard(); const paintedCells = state.paintedCells[cameraFeedID]; + // Get the socket for the current camera only + const cameraASocket = useCameraFeedASocket(); + const cameraBSocket = useCameraFeedBSocket(); + const cameraCSocket = useCameraFeedCSocket(); + + const getCurrentSocket = () => { + switch (cameraFeedID) { + case "A": + return cameraASocket; + case "B": + return cameraBSocket; + case "C": + return cameraCSocket; + } + }; + const handleChange = (e: { target: { value: string } }) => { dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } }); }; @@ -77,6 +99,23 @@ const RegionSelector = ({ setIsResetModalOpen(true); }; + const textClick = (cameraFeedID: "A" | "B" | "C") => { + const socket = getCurrentSocket(); + + // Check if WebSocket is connected + if (socket.readyState !== ReadyState.OPEN) { + toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); + return; + } + + try { + socket.send("ZOOM=0.3,0.3"); + toast.success(`Zoom command sent to Camera ${cameraFeedID}`); + } catch (error) { + console.error("WebSocket send error:", error); + toast.error(`Failed to send command to Camera ${cameraFeedID}`); + } + }; const handleSaveclick = () => { const regions: ColourData[] = []; const test = Array.from(paintedCells.entries()); @@ -189,12 +228,13 @@ const RegionSelector = ({ className="sr-only" />
- Enlarge image + Magnifier {mode === "zoom" && ( Use mouse to digitally zoom in and out )}
+ diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 31d812e..8d0b264 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -64,6 +64,7 @@ const VideoFeedGridPainter = () => { map.delete(key); paintLayerRef.current?.batchDraw(); } + return; } diff --git a/src/utils/config.ts b/src/utils/config.ts index a8ef457..98dd355 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1 +1,3 @@ export const CAMBASE = import.meta.env.VITE_BASEURL; + +export const CAMBASE_WS = import.meta.env.VITE_BASE_WS; -- 2.25.1 From 10e2644666e5fccc25bce196ac93ca5fb652c2f9 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 13:09:07 +0000 Subject: [PATCH 2/6] - improved zoom while clicking on on image to zoom --- src/app/context/WebSocketContext.ts | 4 +- src/app/providers/WebSocketProvider.tsx | 24 +++++-- .../CameraSettings/RegionSelector.tsx | 57 ++++++++------- .../components/Video/VideoFeedGridPainter.tsx | 72 +++++++++++++++++-- src/features/cameras/hooks/useGetVideoFeed.ts | 4 +- .../cameras/hooks/useGetvideoSnapshots.ts | 2 +- src/types/types.ts | 4 ++ 7 files changed, 126 insertions(+), 41 deletions(-) diff --git a/src/app/context/WebSocketContext.ts b/src/app/context/WebSocketContext.ts index dd89d90..551e006 100644 --- a/src/app/context/WebSocketContext.ts +++ b/src/app/context/WebSocketContext.ts @@ -1,6 +1,6 @@ import { createContext, useContext } from "react"; import { ReadyState } from "react-use-websocket"; -import type { InfoBarData } from "../../types/types"; +import type { CameraZoomData, InfoBarData } from "../../types/types"; type InfoSocketState = { data: InfoBarData | null; @@ -10,7 +10,7 @@ type InfoSocketState = { }; type CameraSocketState = { - data: null; + data: CameraZoomData | null; readyState: ReadyState; send: (msg: string) => void; }; diff --git a/src/app/providers/WebSocketProvider.tsx b/src/app/providers/WebSocketProvider.tsx index d2346a2..66ce180 100644 --- a/src/app/providers/WebSocketProvider.tsx +++ b/src/app/providers/WebSocketProvider.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; import { WebsocketContext, type WebSocketConextValue } from "../context/WebSocketContext"; import useWebSocket from "react-use-websocket"; import { wsConfig } from "../config/wsconfig"; -import type { InfoBarData } from "../../types/types"; +import type { CameraZoomData, InfoBarData } from "../../types/types"; type WebSocketProviderProps = { children: ReactNode; @@ -10,7 +10,7 @@ type WebSocketProviderProps = { export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const [systemData, setSystemData] = useState(null); - + const [socketData, setSocketData] = useState(null); const infoSocket = useWebSocket(wsConfig.infoBar, { share: true, shouldReconnect: () => true }); const cameraFeedASocket = useWebSocket(wsConfig.cameraFeedA, { share: true, shouldReconnect: () => true }); const cameraFeedBSocket = useWebSocket(wsConfig.cameraFeedB, { share: true, shouldReconnect: () => true }); @@ -23,9 +23,20 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const data = JSON.parse(text); setSystemData(data); } + if (cameraFeedASocket.lastMessage || cameraFeedBSocket.lastMessage || cameraFeedCSocket.lastMessage) { + const message = cameraFeedASocket.lastMessage || cameraFeedBSocket.lastMessage || cameraFeedCSocket.lastMessage; + const data = await message?.data.text(); + const parsedData: CameraZoomData = JSON.parse(data || ""); + setSocketData(parsedData); + } } parseData(); - }, [infoSocket.lastMessage]); + }, [ + cameraFeedASocket.lastMessage, + cameraFeedBSocket.lastMessage, + cameraFeedCSocket.lastMessage, + infoSocket.lastMessage, + ]); const value = useMemo( () => ({ @@ -35,19 +46,19 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { sendJson: infoSocket.sendJsonMessage, }, cameraFeedA: { - data: null, + data: socketData, readyState: cameraFeedASocket.readyState, send: cameraFeedASocket.sendMessage, }, cameraFeedB: { - data: null, + data: socketData, readyState: cameraFeedBSocket.readyState, send: cameraFeedBSocket.sendMessage, }, cameraFeedC: { - data: null, + data: socketData, readyState: cameraFeedCSocket.readyState, send: cameraFeedCSocket.sendMessage, @@ -62,6 +73,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { cameraFeedCSocket.sendMessage, infoSocket.readyState, infoSocket.sendJsonMessage, + socketData, systemData, ], ); diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index 4b78604..a989bd9 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -4,7 +4,6 @@ import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext" import { useColourDectection } from "../../hooks/useColourDetection"; import { useBlackBoard } from "../../../../hooks/useBlackBoard"; import { toast } from "sonner"; -import { ReadyState } from "react-use-websocket"; import { useCameraFeedASocket, useCameraFeedBSocket, @@ -34,8 +33,6 @@ const RegionSelector = ({ const { state, dispatch } = useCameraFeedContext(); const { blackboardMutation } = useBlackBoard(); const paintedCells = state.paintedCells[cameraFeedID]; - - // Get the socket for the current camera only const cameraASocket = useCameraFeedASocket(); const cameraBSocket = useCameraFeedBSocket(); const cameraCSocket = useCameraFeedCSocket(); @@ -51,6 +48,16 @@ const RegionSelector = ({ } }; + const socket = getCurrentSocket(); + + const getMagnificationLevel = () => { + const test = socket.data; + if (!socket.data) return null; + + if (!test || !test.magnificationLevel) return "0x"; + return test?.magnificationLevel; + }; + const handleChange = (e: { target: { value: string } }) => { dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } }); }; @@ -99,23 +106,6 @@ const RegionSelector = ({ setIsResetModalOpen(true); }; - const textClick = (cameraFeedID: "A" | "B" | "C") => { - const socket = getCurrentSocket(); - - // Check if WebSocket is connected - if (socket.readyState !== ReadyState.OPEN) { - toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); - return; - } - - try { - socket.send("ZOOM=0.3,0.3"); - toast.success(`Zoom command sent to Camera ${cameraFeedID}`); - } catch (error) { - console.error("WebSocket send error:", error); - toast.error(`Failed to send command to Camera ${cameraFeedID}`); - } - }; const handleSaveclick = () => { const regions: ColourData[] = []; const test = Array.from(paintedCells.entries()); @@ -213,6 +203,25 @@ const RegionSelector = ({ /> Erase mode + - diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 8d0b264..02343b9 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -2,8 +2,14 @@ import { useEffect, useRef, useState, type RefObject } from "react"; import { Stage, Layer, Image, Shape } from "react-konva"; import type { KonvaEventObject } from "konva/lib/Node"; import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots"; - import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext"; +import { + useCameraFeedASocket, + useCameraFeedBSocket, + useCameraFeedCSocket, +} from "../../../../app/context/WebSocketContext"; +import { ReadyState } from "react-use-websocket"; +import { toast } from "sonner"; const BACKEND_WIDTH = 640; const BACKEND_HEIGHT = 360; @@ -25,6 +31,7 @@ const VideoFeedGridPainter = () => { const isDrawingRef = useRef(false); const [scale, setScale] = useState(1); const [position, setPosition] = useState({ x: 0, y: 0 }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const stageRef = useRef(null); @@ -34,6 +41,55 @@ const VideoFeedGridPainter = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const paintLayerRef = useRef(null); + const cameraASocket = useCameraFeedASocket(); + const cameraBSocket = useCameraFeedBSocket(); + const cameraCSocket = useCameraFeedCSocket(); + + const getCurrentSocket = () => { + switch (cameraFeedID) { + case "A": + return cameraASocket; + case "B": + return cameraBSocket; + case "C": + return cameraCSocket; + } + }; + + const socket = getCurrentSocket(); + + const getMagnificationLevel = () => { + const test = socket.data; + if (!socket.data) return null; + + if (!test || !test.magnificationLevel) return "0x"; + return test?.magnificationLevel; + }; + + const handleZoomClick = (e: KonvaEventObject, cameraFeedID: "A" | "B" | "C") => { + if (mode !== "zoom") return; + const socket = getCurrentSocket(); + const stage = e.target.getStage(); + const coords = stage?.getPointerPosition(); + if (!coords || !socket) return; + + const newX = coords.x / stageSize.width; + const newY = coords.y / stageSize.height; + + // Check if WebSocket is connected + if (socket.readyState !== ReadyState.OPEN) { + toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); + return; + } + + try { + socket.send(`ZOOM=${newX.toFixed(2)},${newY.toFixed(2)}`); + } catch (error) { + console.error("WebSocket send error:", error); + toast.error(`Failed to send command to Camera ${cameraFeedID}`); + } + }; + const draw = (bmp: RefObject): ImageBitmap | null => { if (!bmp || !bmp.current) { return null; @@ -76,14 +132,14 @@ const VideoFeedGridPainter = () => { }; const handleStageMouseDown = (e: KonvaEventObject) => { - if (!regions[selectedRegionIndex] || mode === "zoom") return; + if (!regions[selectedRegionIndex] || mode === "magnify" || mode === "zoom") return; isDrawingRef.current = true; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); }; const handleStageMouseMove = (e: KonvaEventObject) => { - if (!isDrawingRef.current || mode === "zoom") return; + if (!isDrawingRef.current || mode === "magnify") return; if (!regions[selectedRegionIndex]) return; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); @@ -94,7 +150,7 @@ const VideoFeedGridPainter = () => { }; const handleMouseEnter = () => { - if (mode !== "zoom") return; + if (mode !== "magnify") return; setScale(2); }; const handleMouseLeave = () => { @@ -171,7 +227,13 @@ const VideoFeedGridPainter = () => { x={position.x} y={position.y} > - + handleZoomClick(e, cameraFeedID)} + /> diff --git a/src/features/cameras/hooks/useGetVideoFeed.ts b/src/features/cameras/hooks/useGetVideoFeed.ts index 44b2796..cbee4ac 100644 --- a/src/features/cameras/hooks/useGetVideoFeed.ts +++ b/src/features/cameras/hooks/useGetVideoFeed.ts @@ -28,14 +28,14 @@ export const useGetVideoFeed = (cameraFeedID: "A" | "B" | "C" | null, mode: stri queryKey: ["getfeed", cameraFeedID], queryFn: () => targetDectionFeed(cameraFeedID), refetchInterval: 500, - enabled: mode !== "zoom", + enabled: mode !== "magnify" && mode !== "zoom", }); const videoFeedQuery = useQuery({ queryKey: ["videoQuery", cameraFeedID, mode], queryFn: () => getVideoFeed(cameraFeedID), refetchInterval: 500, - enabled: mode === "zoom", + enabled: mode === "magnify" || mode === "zoom", }); return { targetDetectionQuery, videoFeedQuery }; diff --git a/src/features/cameras/hooks/useGetvideoSnapshots.ts b/src/features/cameras/hooks/useGetvideoSnapshots.ts index da70292..f503584 100644 --- a/src/features/cameras/hooks/useGetvideoSnapshots.ts +++ b/src/features/cameras/hooks/useGetvideoSnapshots.ts @@ -15,7 +15,7 @@ export const useCreateVideoSnapshot = () => { const videoSnapShot = videoFeedQuery?.data; const isVideoLoading = videoFeedQuery.isPending; - if (isVideoLoading === false && videoSnapShot && mode === "zoom") { + if ((isVideoLoading === false && videoSnapShot && mode === "magnify") || mode === "zoom") { snapShot = videoSnapShot; } diff --git a/src/types/types.ts b/src/types/types.ts index a6c1138..0ecad3d 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -11,6 +11,10 @@ export type InfoBarData = { "thread-count": string; }; +export type CameraZoomData = { + magnificationLevel: string; +}; + export type StatusIndicator = "neutral-quaternary" | "dark" | "info" | "success" | "warning" | "danger"; export type Region = { name: string; -- 2.25.1 From eb45eabde9459c19ee4c7b25a53f9119e37d332a Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 14:08:44 +0000 Subject: [PATCH 3/6] - improved magnification level text - removed magnification level --- .../components/CameraSettings/RegionSelector.tsx | 2 +- .../cameras/components/Video/VideoFeedGridPainter.tsx | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index a989bd9..c1e2699 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -238,7 +238,7 @@ const RegionSelector = ({ />
Digital Zoom mode -
{getMagnificationLevel()}
+
{`current Zoom: ${getMagnificationLevel()}`}
{mode === "zoom" && Click image to digitally zoom}
diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 02343b9..e6ea83f 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -56,16 +56,6 @@ const VideoFeedGridPainter = () => { } }; - const socket = getCurrentSocket(); - - const getMagnificationLevel = () => { - const test = socket.data; - if (!socket.data) return null; - - if (!test || !test.magnificationLevel) return "0x"; - return test?.magnificationLevel; - }; - const handleZoomClick = (e: KonvaEventObject, cameraFeedID: "A" | "B" | "C") => { if (mode !== "zoom") return; const socket = getCurrentSocket(); -- 2.25.1 From e395777ae9cebd6c32cbaf9317d85f454c5fe5c9 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 22:32:30 +0000 Subject: [PATCH 4/6] - added modal for entry and exit sightings and plate patches --- .../CameraSettings/RegionSelector.tsx | 12 +- .../PlatePatch/SightingEntryTable.tsx | 123 +++++++++++------- .../PlatePatch/SightingExitTable.tsx | 122 ++++++++++------- .../platePatchModal/PlatePatchModal.tsx | 20 +++ .../PlatePatchModalContent.tsx | 64 +++++++++ .../components/Video/VideoFeedGridPainter.tsx | 6 +- src/types/types.ts | 1 + src/ui/ModalComponent.tsx | 2 +- 8 files changed, 243 insertions(+), 107 deletions(-) create mode 100644 src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModal.tsx create mode 100644 src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModalContent.tsx diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index c1e2699..db1a026 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -152,7 +152,7 @@ const RegionSelector = ({ colourMutation.mutate({ cameraFeedID, regions: regions }); - // Convert Map to plain object for blackboard + // Convert map to plain object for blackboard const serializableState = { ...state, paintedCells: { @@ -282,10 +282,16 @@ const RegionSelector = ({ })}
- -
diff --git a/src/features/cameras/components/PlatePatch/SightingEntryTable.tsx b/src/features/cameras/components/PlatePatch/SightingEntryTable.tsx index b85e795..010c1c3 100644 --- a/src/features/cameras/components/PlatePatch/SightingEntryTable.tsx +++ b/src/features/cameras/components/PlatePatch/SightingEntryTable.tsx @@ -1,73 +1,96 @@ +import { useState } from "react"; import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext"; import type { DecodeReading } from "../../../../types/types"; import { useSightingEntryAndExit } from "../../hooks/useSightingEntryAndExit"; +import PlatePatchModal from "./platePatchModal/PlatePatchModal"; + const SightingEntryTable = () => { const { state } = useCameraFeedContext(); + const [isPlatePatchModalOpen, setIsPlatePatchModalOpen] = useState(false); + const [currentPatch, setCurrentPatch] = useState(null); const cameraFeedID = state.cameraFeedID; const { entryQuery } = useSightingEntryAndExit(cameraFeedID); const isLoading = entryQuery?.isFetching; const readings = entryQuery?.data?.decodes; + const handleRowClick = (reading: DecodeReading) => { + setCurrentPatch(reading); + setIsPlatePatchModalOpen(true); + }; + if (isLoading) return Loading Sighting data…; return ( -
- {/* Desktop Table */} -
- - - - - - - - - - - - {readings?.map((reading: DecodeReading) => ( - - - - - - + <> +
+ {/* Desktop Table */} +
+
VRMBay IDSeen CountFirst SeenLast Seen
{reading?.vrm}{reading?.laneID}{reading?.seenCount}{reading?.firstSeenTimeHumane}{reading?.lastSeenTimeHumane}
+ + + + + + + - ))} - -
VRMBay IDSeen CountFirst SeenLast Seen
-
+ + + {readings?.map((reading: DecodeReading) => ( + handleRowClick(reading)} + > + {reading?.vrm} + {reading?.laneID} + {reading?.seenCount} + {reading?.firstSeenTimeHumane} + {reading?.lastSeenTimeHumane} + + ))} + + +
- {/* Mobile Cards */} -
- {readings?.map((reading: DecodeReading) => ( -
-
- {reading?.vrm} - Bay {reading?.laneID} -
-
- Seen Count: - {reading?.seenCount} -
-
-
- First Seen: - {reading?.firstSeenTimeHumane} + {/* Mobile */} +
+ {readings?.map((reading: DecodeReading) => ( +
handleRowClick(reading)} + > +
+ {reading?.vrm} + Bay {reading?.laneID}
-
- Last Seen: - {reading?.lastSeenTimeHumane} +
+ Seen Count: + {reading?.seenCount} +
+
+
+ First Seen: + {reading?.firstSeenTimeHumane} +
+
+ Last Seen: + {reading?.lastSeenTimeHumane} +
-
- ))} + ))} +
-
+ setIsPlatePatchModalOpen(false)} + currentPatch={currentPatch} + direction={"entry"} + /> + ); }; diff --git a/src/features/cameras/components/PlatePatch/SightingExitTable.tsx b/src/features/cameras/components/PlatePatch/SightingExitTable.tsx index 3ba01f9..62e3acf 100644 --- a/src/features/cameras/components/PlatePatch/SightingExitTable.tsx +++ b/src/features/cameras/components/PlatePatch/SightingExitTable.tsx @@ -1,8 +1,12 @@ +import { useState } from "react"; import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext"; import type { DecodeReading } from "../../../../types/types"; import { useSightingEntryAndExit } from "../../hooks/useSightingEntryAndExit"; +import PlatePatchModal from "./platePatchModal/PlatePatchModal"; const SightingExitTable = () => { + const [isPlatePatchModalOpen, setIsPlatePatchModalOpen] = useState(false); + const [currentPatch, setCurrentPatch] = useState(null); const { state } = useCameraFeedContext(); const cameraFeedID = state.cameraFeedID; const { exitQuery } = useSightingEntryAndExit(cameraFeedID); @@ -10,64 +14,82 @@ const SightingExitTable = () => { const isLoading = exitQuery?.isFetching; const readings = exitQuery?.data?.decodes; + const handleRowClick = (reading: DecodeReading) => { + setCurrentPatch(reading); + setIsPlatePatchModalOpen(true); + }; + if (isLoading) return Loading Sighting data…; return ( -
- {/* Desktop Table */} -
- - - - - - - - - - - - {readings?.map((reading: DecodeReading) => ( - - - - - - + <> +
+ {/* Desktop Table */} +
+
VRMBay IDSeen CountFirst SeenLast Seen
{reading?.vrm}{reading?.laneID}{reading?.seenCount}{reading?.firstSeenTimeHumane}{reading?.lastSeenTimeHumane}
+ + + + + + + - ))} - -
VRMBay IDSeen CountFirst SeenLast Seen
-
+ + + {readings?.map((reading: DecodeReading) => ( + handleRowClick(reading)} + > + {reading?.vrm} + {reading?.laneID} + {reading?.seenCount} + {reading?.firstSeenTimeHumane} + {reading?.lastSeenTimeHumane} + + ))} + + +
- {/* Mobile Cards */} -
- {readings?.map((reading: DecodeReading) => ( -
-
- {reading?.vrm} - Bay {reading?.laneID} -
-
- Seen Count: - {reading?.seenCount} -
-
-
- First Seen: - {reading?.firstSeenTimeHumane} + {/* Mobile Cards */} +
+ {readings?.map((reading: DecodeReading) => ( +
handleRowClick(reading)} + > +
+ {reading?.vrm} + Bay {reading?.laneID}
-
- Last Seen: - {reading?.lastSeenTimeHumane} +
+ Seen Count: + {reading?.seenCount} +
+
+
+ First Seen: + {reading?.firstSeenTimeHumane} +
+
+ Last Seen: + {reading?.lastSeenTimeHumane} +
-
- ))} + ))} +
-
+ setIsPlatePatchModalOpen(false)} + currentPatch={currentPatch} + direction={"exit"} + /> + ); }; diff --git a/src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModal.tsx b/src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModal.tsx new file mode 100644 index 0000000..9c223e3 --- /dev/null +++ b/src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModal.tsx @@ -0,0 +1,20 @@ +import type { DecodeReading } from "../../../../../types/types"; +import ModalComponent from "../../../../../ui/ModalComponent"; +import PlatePatchModalContent from "./PlatePatchModalContent"; + +type PlatePatchModalProps = { + isPlatePatchModalOpen: boolean; + handleClose: () => void; + currentPatch: DecodeReading | null; + direction?: "entry" | "exit"; +}; + +const PlatePatchModal = ({ isPlatePatchModalOpen, handleClose, currentPatch, direction }: PlatePatchModalProps) => { + return ( + + + + ); +}; + +export default PlatePatchModal; diff --git a/src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModalContent.tsx b/src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModalContent.tsx new file mode 100644 index 0000000..a5c8477 --- /dev/null +++ b/src/features/cameras/components/PlatePatch/platePatchModal/PlatePatchModalContent.tsx @@ -0,0 +1,64 @@ +import type { DecodeReading } from "../../../../../types/types"; + +type PlatePatchModalContentProps = { + currentPatch: DecodeReading | null; + direction?: "entry" | "exit"; +}; + +const PlatePatchModalContent = ({ currentPatch, direction }: PlatePatchModalContentProps) => { + const imageSrc = `data:image/png;base64,${currentPatch?.plate || ""}`; + const imageUrl = currentPatch ? imageSrc : ""; + + return ( +
+
+

+ {currentPatch?.vrm} +

+ + {direction === "entry" ? "Entry" : "Exit"} + +
+ +
+
+ {`${direction +
+ +
+
+

Bay ID

+

{currentPatch?.laneID || "N/A"}

+
+ +
+

Seen Count

+

{currentPatch?.seenCount || "N/A"}

+
+ +
+

First Seen

+

{currentPatch?.firstSeenTimeHumane || "N/A"}

+
+ +
+

Last Seen

+

{currentPatch?.lastSeenTimeHumane || "N/A"}

+
+
+
+
+ ); +}; + +export default PlatePatchModalContent; diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index e6ea83f..fd38f24 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -35,12 +35,12 @@ const VideoFeedGridPainter = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const stageRef = useRef(null); - const currentScale = stageSize.width / BACKEND_WIDTH; - const size = BACKEND_CELL_SIZE * currentScale; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const paintLayerRef = useRef(null); + const currentScale = stageSize.width / BACKEND_WIDTH; + const size = BACKEND_CELL_SIZE * currentScale; + const cameraASocket = useCameraFeedASocket(); const cameraBSocket = useCameraFeedBSocket(); const cameraCSocket = useCameraFeedCSocket(); diff --git a/src/types/types.ts b/src/types/types.ts index 0ecad3d..b7dce94 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -202,6 +202,7 @@ export type DecodeReading = { duplicate?: true; firstSeenTimeHumane: string; lastSeenTimeHumane: string; + plate?: string; }; export type ColourData = { diff --git a/src/ui/ModalComponent.tsx b/src/ui/ModalComponent.tsx index 3352678..92b1b49 100644 --- a/src/ui/ModalComponent.tsx +++ b/src/ui/ModalComponent.tsx @@ -26,7 +26,7 @@ const ModalComponent = ({ isModalOpen, children, close }: ModalComponentProps) =
-- 2.25.1 From d1995f0a9fac7c94d4f153dc1e117c4c4cabc018 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Thu, 11 Dec 2025 10:52:13 +0000 Subject: [PATCH 5/6] - updated main endpoint end point and added flexibility for camera navigation --- src/utils/config.ts | 10 ++++++++-- vite.config.ts | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/config.ts b/src/utils/config.ts index 98dd355..bebcc8d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,3 +1,9 @@ -export const CAMBASE = import.meta.env.VITE_BASEURL; +export const cambase = import.meta.env.VITE_BASEURL; -export const CAMBASE_WS = import.meta.env.VITE_BASE_WS; +export const CAMBASEWS = import.meta.env.VITE_BASE_WS; + +const environment = import.meta.env.MODE; + +export const CAMBASE = environment === "development" ? cambase : window.location.origin; + +export const CAMBASE_WS = environment === "development" ? CAMBASEWS : window.location.origin.replace(/^http/, "ws"); diff --git a/vite.config.ts b/vite.config.ts index 7b8f3b9..9c51349 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,7 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite"; // https://vite.dev/config/ export default defineConfig({ + base: "/bayiq", plugins: [ tanstackRouter({ target: "react", -- 2.25.1 From 3353ad6f8b922f3c2a45372eda17bde7fa7eb26a Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 12 Dec 2025 08:32:06 +0000 Subject: [PATCH 6/6] - updated paths and code splitting config --- src/main.tsx | 5 ++++- vite.config.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 90b08ec..857fb22 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,7 +6,10 @@ import { AppProviders } from "./app/providers/AppProviders"; import "./index.css"; import Modal from "react-modal"; -const router = createRouter({ routeTree }); +const router = createRouter({ + routeTree, + basepath: "/bayiq", +}); Modal.setAppElement("#root"); diff --git a/vite.config.ts b/vite.config.ts index 9c51349..d367053 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ plugins: [ tanstackRouter({ target: "react", - autoCodeSplitting: true, + autoCodeSplitting: false, }), react(), tailwindcss(), @@ -22,4 +22,11 @@ export default defineConfig({ }, }, }, + build: { + rollupOptions: { + output: { + manualChunks: undefined, + }, + }, + }, }); -- 2.25.1