added sighting feed
This commit is contained in:
43
src/hooks/useHiDPICanvas.ts
Normal file
43
src/hooks/useHiDPICanvas.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useHiDPICanvas(
|
||||
imgRef: React.RefObject<HTMLImageElement | null>,
|
||||
canvasRef: React.RefObject<HTMLCanvasElement | null>
|
||||
) {
|
||||
const sync = () => {
|
||||
const img = imgRef.current,
|
||||
cvs = canvasRef.current;
|
||||
if (!img || !cvs) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = img.clientWidth || img.naturalWidth || 0;
|
||||
const h = img.clientHeight || img.naturalHeight || 0;
|
||||
|
||||
// CSS size
|
||||
cvs.style.width = `${w}px`;
|
||||
cvs.style.height = `${h}px`;
|
||||
|
||||
// backing store size (scaled for HiDPI)
|
||||
const W = Math.max(1, Math.round(w * dpr));
|
||||
const H = Math.max(1, Math.round(h * dpr));
|
||||
if (cvs.width !== W || cvs.height !== H) {
|
||||
cvs.width = W;
|
||||
cvs.height = H;
|
||||
const ctx = cvs.getContext("2d");
|
||||
if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS px
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ro = new ResizeObserver(sync); // reacts to image size changes
|
||||
if (imgRef.current) ro.observe(imgRef.current);
|
||||
const onResize = () => sync();
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return { sync };
|
||||
}
|
||||
37
src/hooks/useOverviewOverlay.ts
Normal file
37
src/hooks/useOverviewOverlay.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect } from "react";
|
||||
import type { SightingWidgetType } from "../types/types";
|
||||
import { drawRects } from "../utils/utils";
|
||||
|
||||
type Mode = 0 | 1 | 2;
|
||||
|
||||
export function useOverviewOverlay(
|
||||
selected: SightingWidgetType | null | undefined,
|
||||
overlayMode: Mode,
|
||||
imgRef: React.RefObject<HTMLImageElement | null>,
|
||||
canvasRef: React.RefObject<HTMLCanvasElement | null>
|
||||
) {
|
||||
useEffect(() => {
|
||||
const img = imgRef?.current;
|
||||
const cvs = canvasRef?.current;
|
||||
if (!img || !cvs) return;
|
||||
|
||||
const ctx = cvs.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// clear
|
||||
ctx.clearRect(0, 0, cvs.width, cvs.height);
|
||||
|
||||
if (!selected || overlayMode === 0) return;
|
||||
|
||||
if (overlayMode === 1 && selected.overviewPlateRect) {
|
||||
drawRects(cvs, img, [selected.overviewPlateRect], "chartreuse");
|
||||
} else if (overlayMode === 2) {
|
||||
const rects = selected.plateTrack ?? [];
|
||||
if (rects.length) drawRects(cvs, img, rects, "yellow");
|
||||
if (selected.overviewPlateRect) {
|
||||
drawRects(cvs, img, [selected.overviewPlateRect], "chartreuse");
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected?.ref, overlayMode, imgRef?.current?.src]);
|
||||
}
|
||||
91
src/hooks/useSightingFeed.ts
Normal file
91
src/hooks/useSightingFeed.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { SightingWidgetType } from "../types/types";
|
||||
|
||||
export function useSightingFeed(
|
||||
baseUrl: string,
|
||||
{
|
||||
limit = 7,
|
||||
pollMs = 800,
|
||||
autoSelectLatest = true,
|
||||
}: {
|
||||
limit?: number;
|
||||
pollMs?: number;
|
||||
autoSelectLatest?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const [items, setItems] = useState<SightingWidgetType[]>(
|
||||
() => Array(limit).fill(null) as unknown as SightingWidgetType[]
|
||||
);
|
||||
const [selectedRef, setSelectedRef] = useState<number | null>(null);
|
||||
const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null);
|
||||
|
||||
const mostRecentRef = useRef<number>(-1);
|
||||
|
||||
// effective selected (fallback to most recent)
|
||||
const selected = useMemo(
|
||||
() =>
|
||||
selectedRef == null
|
||||
? null
|
||||
: items.find((x) => x?.ref === selectedRef) ?? null,
|
||||
[items, selectedRef]
|
||||
);
|
||||
const effectiveSelected = selected ?? mostRecent ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
let delay = pollMs;
|
||||
let dead = false;
|
||||
const controller = new AbortController();
|
||||
|
||||
async function tick() {
|
||||
try {
|
||||
// Pause when tab hidden to save CPU/network
|
||||
if (document.hidden) {
|
||||
setTimeout(tick, Math.max(delay, 2000));
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=${mostRecentRef.current}`;
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
|
||||
const obj: SightingWidgetType = await res.json();
|
||||
if (obj && typeof obj.ref === "number" && obj.ref > -1) {
|
||||
setItems((prev) => {
|
||||
const next = [obj, ...prev].slice(0, limit);
|
||||
// maintain selection if still present; otherwise select newest if allowed
|
||||
const stillExists =
|
||||
selectedRef != null && next.some((x) => x?.ref === selectedRef);
|
||||
if (autoSelectLatest && !stillExists) {
|
||||
setSelectedRef(obj.ref);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setMostRecent(obj);
|
||||
mostRecentRef.current = obj.ref;
|
||||
delay = pollMs; // reset backoff on success
|
||||
}
|
||||
} catch {
|
||||
// exponential backoff (max 10s)
|
||||
delay = Math.min(delay * 2, 10000);
|
||||
} finally {
|
||||
if (!dead) setTimeout(tick, delay);
|
||||
}
|
||||
}
|
||||
|
||||
const t = setTimeout(tick, pollMs);
|
||||
return () => {
|
||||
dead = true;
|
||||
controller.abort();
|
||||
clearTimeout(t);
|
||||
};
|
||||
}, [baseUrl, limit, pollMs, autoSelectLatest, selectedRef]);
|
||||
|
||||
return {
|
||||
items,
|
||||
selectedRef,
|
||||
setSelectedRef,
|
||||
mostRecent,
|
||||
effectiveSelected,
|
||||
mostRecentRef,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user