- fixed cropped live feed and amended APIs for submission
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
153
src/hooks/useGetOverviewSnapshot copy.ts
Normal file
153
src/hooks/useGetOverviewSnapshot copy.ts
Normal 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 };
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await 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) {
|
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) {
|
||||||
img.onload = () => {
|
bitmap.close();
|
||||||
drawImage();
|
return;
|
||||||
};
|
|
||||||
img.src = imgUrl;
|
|
||||||
|
|
||||||
if (latestUrlRef.current) {
|
|
||||||
URL.revokeObjectURL(latestUrlRef.current);
|
|
||||||
}
|
}
|
||||||
latestUrlRef.current = imgUrl;
|
|
||||||
|
// 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 () => {
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user