diff --git a/src/components/CameraOverview/SnapshotContainer.tsx b/src/components/CameraOverview/SnapshotContainer.tsx index 3065c3e..0efc454 100644 --- a/src/components/CameraOverview/SnapshotContainer.tsx +++ b/src/components/CameraOverview/SnapshotContainer.tsx @@ -25,18 +25,14 @@ export const SnapshotContainer = ({ side, settingsPage, zoomLevel, onZoomLevelCh return (
-
+
{isError && } {isPending && ( -
+
)} - +
); diff --git a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx index 3f0be06..44bb0a4 100644 --- a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx +++ b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx @@ -30,7 +30,7 @@ const OverviewVideoContainer = ({ trackMouse: true, }); return ( - +
{ laneId: laneIdQuery?.data?.id, LID1: values.LID1, LID2: values.LID2, - LID3: values.LID3, }; await bof2LandMutation.mutateAsync(bof2LaneData); await backOfficeDispatcherMutation.mutateAsync(bof2ConstantsData); diff --git a/src/hooks/useCameraOutput.ts b/src/hooks/useCameraOutput.ts index 4bdb9ca..641c4ac 100644 --- a/src/hooks/useCameraOutput.ts +++ b/src/hooks/useCameraOutput.ts @@ -92,7 +92,7 @@ const updateBOF2LaneId = async (data: OptionalBOF2LaneIDs) => { }; const getBOF2LaneId = async () => { - const response = await fetch(`${CAM_BASE}/api/fetch-config?id=SightingAmmendA-lane-ids`); + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=SightingAmmend-lane-ids`); if (!response.ok) throw new Error("Canot get Lane Ids"); return response.json(); }; diff --git a/src/hooks/useGetOverviewSnapshot copy.ts b/src/hooks/useGetOverviewSnapshot copy.ts new file mode 100644 index 0000000..6bed2e3 --- /dev/null +++ b/src/hooks/useGetOverviewSnapshot copy.ts @@ -0,0 +1,153 @@ +import { useRef, useCallback, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { CAM_BASE } from "../utils/config"; + +const apiUrl = CAM_BASE; + +async function fetchSnapshot(cameraSide: string): Promise { + const response = await fetch(`${apiUrl}/${cameraSide}-preview`, { + signal: AbortSignal.timeout(300000), + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`Cannot reach endpoint (${response.status})`); + } + return response.blob(); +} + +/** Draw an ImageBitmap to canvas with aspect-fill (like object-fit: cover) */ +function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const cssWidth = canvas.clientWidth; + const cssHeight = canvas.clientHeight; + + const width = Math.floor(cssWidth * dpr); + const height = Math.floor(cssHeight * dpr); + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } + + ctx.clearRect(0, 0, width, height); + + const srcW = bitmap.width; + const srcH = bitmap.height; + const srcAspect = srcW / srcH; + const dstAspect = width / height; + + let drawWidth = width; + let drawHeight = height; + + // aspect-fit calculation (no cropping) + if (srcAspect > dstAspect) { + // image is wider → fit to canvas width + drawWidth = width; + drawHeight = width / srcAspect; + } else { + // image is taller → fit to canvas height + drawHeight = height; + drawWidth = height * srcAspect; + } + + // center image (adds black borders if aspect ratios differ) + const dx = (width - drawWidth) / 50; + const dy = (height - drawHeight) / 2; + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.drawImage(bitmap, 0, 0, srcW, srcH, dx, dy, drawWidth, drawHeight); +} + +export function useGetOverviewSnapshot(side: string) { + const canvasRef = useRef(null); + const latestBitmapRef = useRef(null); + + // Redraw helper; always draws the current bitmap if available + const draw = useCallback(() => { + const canvas = canvasRef.current; + const bmp = latestBitmapRef.current; + if (!canvas || !bmp) return; + drawBitmapToCanvas(canvas, bmp); + }, []); + + const { + data: snapshotBlob, + isError, + error, + isPending, + } = useQuery({ + queryKey: ["overviewSnapshot", side], + queryFn: () => fetchSnapshot(side), + // Poll ~4 fps when visible; pause when tab hidden + refetchInterval: () => (document.visibilityState === "visible" ? 250 : false), + refetchOnWindowFocus: false, + // Avoid keeping lots of blobs around in cache + gcTime: 0, // v5 name (cacheTime in v4) + staleTime: 0, + retry: false, // or a small number if you prefer retries + }); + + // Convert Blob -> ImageBitmap and draw + useEffect(() => { + let cancelled = false; + if (!snapshotBlob) return; + + (async () => { + try { + const bitmap = await createImageBitmap(snapshotBlob); + if (cancelled) { + bitmap.close(); + return; + } + + // Dispose previous bitmap to free memory + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + } + latestBitmapRef.current = bitmap; + + // Draw now (and again on next resize) + draw(); + } catch { + // noop — fetch handler surfaces the main error path + } + })(); + + return () => { + cancelled = true; + }; + }, [snapshotBlob, draw]); + + // Redraw on resize & DPR changes + useEffect(() => { + const onResize = () => draw(); + const onDPR = () => draw(); + window.addEventListener("resize", onResize); + // Listen for DPR changes (some browsers support this) + const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); + mql.addEventListener?.("change", onDPR); + return () => { + window.removeEventListener("resize", onResize); + mql.removeEventListener?.("change", onDPR); + }; + }, [draw]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + latestBitmapRef.current = null; + } + }; + }, []); + + // Optional: normalize error type + const typedError = error instanceof Error ? error : undefined; + + return { canvasRef, isError, error: typedError, isPending }; +} diff --git a/src/hooks/useGetOverviewSnapshot.ts b/src/hooks/useGetOverviewSnapshot.ts index dfd2090..3b1f513 100644 --- a/src/hooks/useGetOverviewSnapshot.ts +++ b/src/hooks/useGetOverviewSnapshot.ts @@ -3,34 +3,75 @@ import { useQuery } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; const apiUrl = CAM_BASE; -// const fetch_url = `http://100.82.205.44/Colour-preview`; -async function fetchSnapshot(cameraSide: string) { + +async function fetchSnapshot(cameraSide: string): Promise { const response = await fetch(`${apiUrl}/${cameraSide}-preview`, { signal: AbortSignal.timeout(300000), + cache: "no-store", }); if (!response.ok) { - throw new Error("Cannot reach endpoint"); + throw new Error(`Cannot reach endpoint (${response.status})`); + } + return response.blob(); +} + +/** Draw an ImageBitmap to canvas with aspect-fill (like object-fit: cover) */ +function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const cssWidth = canvas.clientWidth; + const cssHeight = canvas.clientHeight; + + const width = Math.floor(cssWidth * dpr); + const height = Math.floor(cssHeight * dpr); + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; } - return await response.blob(); + ctx.clearRect(0, 0, width, height); + + const srcW = bitmap.width; + const srcH = bitmap.height; + const srcAspect = srcW / srcH; + const dstAspect = width / height; + + let drawWidth = width; + let drawHeight = height; + + // aspect-fit calculation (no cropping) + if (srcAspect > dstAspect) { + // image is wider → fit to canvas width + drawWidth = width; + drawHeight = width / srcAspect; + } else { + // image is taller → fit to canvas height + drawHeight = height; + drawWidth = height * srcAspect; + } + + // center image (adds black borders if aspect ratios differ) + const dx = (width - drawWidth) / 50; + const dy = (height - drawHeight) / 2; + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.drawImage(bitmap, 0, 0, srcW, srcH, dx, dy, drawWidth, drawHeight); } export function useGetOverviewSnapshot(side: string) { - const latestUrlRef = useRef(null); const canvasRef = useRef(null); - const imageRef = useRef(null); + const latestBitmapRef = useRef(null); - const drawImage = useCallback(() => { + // Redraw helper; always draws the current bitmap if available + const draw = useCallback(() => { const canvas = canvasRef.current; - const ctx = canvas?.getContext("2d"); - const img = imageRef.current; - - if (!canvas || !ctx || !img) return; - - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; - - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const bmp = latestBitmapRef.current; + if (!canvas || !bmp) return; + drawBitmapToCanvas(canvas, bmp); }, []); const { @@ -39,43 +80,82 @@ export function useGetOverviewSnapshot(side: string) { error, isPending, } = useQuery({ - queryKey: ["overviewSnapshot"], + queryKey: ["overviewSnapshot", side], queryFn: () => fetchSnapshot(side), + // Poll ~4 fps when visible; pause when tab hidden + refetchInterval: () => (document.visibilityState === "visible" ? 250 : false), refetchOnWindowFocus: false, - refetchInterval: 250, + // Avoid keeping lots of blobs around in cache + gcTime: 0, // v5 name (cacheTime in v4) + staleTime: 0, + retry: false, // or a small number if you prefer retries }); + // Convert Blob -> ImageBitmap and draw useEffect(() => { + let cancelled = false; if (!snapshotBlob) return; - const imgUrl = URL.createObjectURL(snapshotBlob); - const img = new Image(); - imageRef.current = img; + (async () => { + try { + const bitmap = await createImageBitmap(snapshotBlob); + if (cancelled) { + bitmap.close(); + return; + } - img.onload = () => { - drawImage(); - }; - img.src = imgUrl; + // Dispose previous bitmap to free memory + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + } + latestBitmapRef.current = bitmap; - if (latestUrlRef.current) { - URL.revokeObjectURL(latestUrlRef.current); - } - latestUrlRef.current = imgUrl; + // Draw now (and again on next resize) + draw(); + } catch { + // noop — fetch handler surfaces the main error path + } + })(); return () => { - if (latestUrlRef.current) { - URL.revokeObjectURL(latestUrlRef.current); - latestUrlRef.current = null; - } + cancelled = true; }; - }, [snapshotBlob, drawImage]); + }, [snapshotBlob, draw]); + + // Redraw on resize & DPR changes + useEffect(() => { + const onResize = () => draw(); + const onDPR = () => draw(); + window.addEventListener("resize", onResize); + // Listen for DPR changes (some browsers support this) + const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); + mql.addEventListener?.("change", onDPR); + return () => { + window.removeEventListener("resize", onResize); + mql.removeEventListener?.("change", onDPR); + }; + }, [draw]); useEffect(() => { - window.addEventListener("resize", drawImage); - return () => { - window.removeEventListener("resize", drawImage); - }; - }, [drawImage]); + const el = canvasRef.current?.parentElement; // the box + if (!el) return; + const ro = new ResizeObserver(() => draw()); // your draw() calls aspect-fit logic + ro.observe(el); + return () => ro.disconnect(); + }, [draw]); - return { canvasRef, isError, error, isPending }; + // Cleanup on unmount + useEffect(() => { + return () => { + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + latestBitmapRef.current = null; + } + }; + }, []); + + // Optional: normalize error type + const typedError = error instanceof Error ? error : undefined; + + return { canvasRef, isError, error: typedError, isPending }; } diff --git a/src/hooks/useSightingAmend.ts b/src/hooks/useSightingAmend.ts index 3c2dd42..c85a7cf 100644 --- a/src/hooks/useSightingAmend.ts +++ b/src/hooks/useSightingAmend.ts @@ -3,14 +3,14 @@ import { CAM_BASE } from "../utils/config"; import type { InitialValuesForm } from "../types/types"; const getSightingAmend = async () => { - const response = await fetch(`${CAM_BASE}/api/fetch-config?id=SightingAmmendA`); + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=SightingAmmend`); if (!response.ok) throw new Error("Cannot reach sighting amend endpoint"); return response.json(); }; const updateSightingAmend = async (data: InitialValuesForm) => { const updateSightingAmendPayload = { - id: "SightingAmmendA", + id: "SightingAmmend", fields: [ { property: "propOverviewQuality", @@ -22,7 +22,7 @@ const updateSightingAmend = async (data: InitialValuesForm) => { }, ], }; - const response = await fetch(`${CAM_BASE}/api/update-config?id=SightingAmmendA`, { + const response = await fetch(`${CAM_BASE}/api/update-config`, { method: "Post", body: JSON.stringify(updateSightingAmendPayload), });