- fixed cropped live feed and amended APIs for submission

This commit is contained in:
2025-11-10 11:55:15 +00:00
parent a734de6261
commit ddeedd2d72
7 changed files with 281 additions and 53 deletions

View File

@@ -25,18 +25,14 @@ export const SnapshotContainer = ({ side, settingsPage, zoomLevel, onZoomLevelCh
return ( return (
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row">
<NavigationArrow side={side} settingsPage={settingsPage} /> <NavigationArrow side={side} settingsPage={settingsPage} />
<div className="w-full"> <div className="w-full bg-[#253445] rounded-md overflow-hidden md:h-[500px] lg:h-[70vh]">
{isError && <ErrorState />} {isError && <ErrorState />}
{isPending && ( {isPending && (
<div className="my-50 h-[50%]"> <div className="absolute inset-0 grid place-items-center">
<Loading message="Camera Preview" /> <Loading message="Camera Preview" />
</div> </div>
)} )}
<canvas <canvas onClick={handleZoomClick} ref={canvasRef} className="block w-full h-full z-20" />
onClick={handleZoomClick}
ref={canvasRef}
className="absolute inset-0 object-contain min-h-[100%] z-20"
/>
</div> </div>
</div> </div>
); );

View File

@@ -30,7 +30,7 @@ const OverviewVideoContainer = ({
trackMouse: true, trackMouse: true,
}); });
return ( return (
<Card className={clsx("relative min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[70%] overflow-y-hidden")}> <Card className={clsx("relative min-h-[40vh] md:min-h-[40vh] max-h-[70vh] lg:w-[70%] overflow-y-hidden")}>
<div className="w-full" {...handlers}> <div className="w-full" {...handlers}>
<SnapshotContainer <SnapshotContainer
side={side} side={side}

View File

@@ -114,7 +114,6 @@ const SettingForms = () => {
laneId: laneIdQuery?.data?.id, laneId: laneIdQuery?.data?.id,
LID1: values.LID1, LID1: values.LID1,
LID2: values.LID2, LID2: values.LID2,
LID3: values.LID3,
}; };
await bof2LandMutation.mutateAsync(bof2LaneData); await bof2LandMutation.mutateAsync(bof2LaneData);
await backOfficeDispatcherMutation.mutateAsync(bof2ConstantsData); await backOfficeDispatcherMutation.mutateAsync(bof2ConstantsData);

View File

@@ -92,7 +92,7 @@ const updateBOF2LaneId = async (data: OptionalBOF2LaneIDs) => {
}; };
const getBOF2LaneId = async () => { 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"); if (!response.ok) throw new Error("Canot get Lane Ids");
return response.json(); return response.json();
}; };

View File

@@ -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<Blob> {
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<HTMLCanvasElement | null>(null);
const latestBitmapRef = useRef<ImageBitmap | null>(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 };
}

View File

@@ -3,34 +3,75 @@ import { useQuery } from "@tanstack/react-query";
import { CAM_BASE } from "../utils/config"; import { CAM_BASE } from "../utils/config";
const apiUrl = CAM_BASE; 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<Blob> {
const response = await fetch(`${apiUrl}/${cameraSide}-preview`, { const response = await fetch(`${apiUrl}/${cameraSide}-preview`, {
signal: AbortSignal.timeout(300000), signal: AbortSignal.timeout(300000),
cache: "no-store",
}); });
if (!response.ok) { 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) { export function useGetOverviewSnapshot(side: string) {
const latestUrlRef = useRef<string | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null); const latestBitmapRef = useRef<ImageBitmap | null>(null);
const drawImage = useCallback(() => { // Redraw helper; always draws the current bitmap if available
const draw = useCallback(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d"); const bmp = latestBitmapRef.current;
const img = imageRef.current; if (!canvas || !bmp) return;
drawBitmapToCanvas(canvas, bmp);
if (!canvas || !ctx || !img) return;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}, []); }, []);
const { const {
@@ -39,43 +80,82 @@ export function useGetOverviewSnapshot(side: string) {
error, error,
isPending, isPending,
} = useQuery({ } = useQuery({
queryKey: ["overviewSnapshot"], queryKey: ["overviewSnapshot", side],
queryFn: () => fetchSnapshot(side), queryFn: () => fetchSnapshot(side),
// Poll ~4 fps when visible; pause when tab hidden
refetchInterval: () => (document.visibilityState === "visible" ? 250 : false),
refetchOnWindowFocus: 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(() => { useEffect(() => {
let cancelled = false;
if (!snapshotBlob) return; if (!snapshotBlob) return;
const imgUrl = URL.createObjectURL(snapshotBlob); (async () => {
const img = new Image(); try {
imageRef.current = img; const bitmap = await createImageBitmap(snapshotBlob);
if (cancelled) {
bitmap.close();
return;
}
img.onload = () => { // Dispose previous bitmap to free memory
drawImage(); if (latestBitmapRef.current) {
}; latestBitmapRef.current.close();
img.src = imgUrl; }
latestBitmapRef.current = bitmap;
if (latestUrlRef.current) { // Draw now (and again on next resize)
URL.revokeObjectURL(latestUrlRef.current); draw();
} } catch {
latestUrlRef.current = imgUrl; // noop — fetch handler surfaces the main error path
}
})();
return () => { return () => {
if (latestUrlRef.current) { cancelled = true;
URL.revokeObjectURL(latestUrlRef.current);
latestUrlRef.current = null;
}
}; };
}, [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(() => { useEffect(() => {
window.addEventListener("resize", drawImage); const el = canvasRef.current?.parentElement; // the box
return () => { if (!el) return;
window.removeEventListener("resize", drawImage); const ro = new ResizeObserver(() => draw()); // your draw() calls aspect-fit logic
}; ro.observe(el);
}, [drawImage]); 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 };
} }

View File

@@ -3,14 +3,14 @@ import { CAM_BASE } from "../utils/config";
import type { InitialValuesForm } from "../types/types"; import type { InitialValuesForm } from "../types/types";
const getSightingAmend = async () => { 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"); if (!response.ok) throw new Error("Cannot reach sighting amend endpoint");
return response.json(); return response.json();
}; };
const updateSightingAmend = async (data: InitialValuesForm) => { const updateSightingAmend = async (data: InitialValuesForm) => {
const updateSightingAmendPayload = { const updateSightingAmendPayload = {
id: "SightingAmmendA", id: "SightingAmmend",
fields: [ fields: [
{ {
property: "propOverviewQuality", 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", method: "Post",
body: JSON.stringify(updateSightingAmendPayload), body: JSON.stringify(updateSightingAmendPayload),
}); });