More messing about, and learning how it all works. Added time to header. Changed some padding. Changed video to preview not target detection. Added TODO list.
This commit is contained in:
18
TODO.txt
Normal file
18
TODO.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
TODO:
|
||||||
|
|
||||||
|
Hotlist upload (Question for Dion about API) and hits popping up in sighting stack.
|
||||||
|
NPED API working and catagories popping up in sighting stack. Images added to public folder.
|
||||||
|
Make the friendly name of each camera permeate throughout.
|
||||||
|
Make favicon MAV logo.
|
||||||
|
Swipe down to get to session page.
|
||||||
|
I have made an error I don't know how to fix in SightingFeedProvider.tsx
|
||||||
|
There is a bug in /front-camera-settings where the navigation arrow doesn't have a transparent background. I don't know why it is only that one and I can't find out why. Very strange.
|
||||||
|
The selected sighting in the sighting stack seems a tad buggy. Sometimes multiple get selected.
|
||||||
|
Can the selected sighting be shown in full detail. How this will look is still up for debate. Either as a pop up card as in AiQ Flexi, or in the OVerview card??
|
||||||
|
How do you know if the time has sync? Make UTC red if not sync.
|
||||||
|
Can the relative aspect ratio in SightingOverview.tsx be the ratio of image pixel size of the image to best take advantage of the space?
|
||||||
|
|
||||||
|
|
||||||
|
FYI:
|
||||||
|
|
||||||
|
Session, WiFi and Modem stuff isn't implimented in the backend. Those are just placeholders for now.
|
||||||
BIN
public/homepage-banner.jpg
Normal file
BIN
public/homepage-banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -26,7 +26,7 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
||||||
<CardHeader title="Front Overiew" icon={faCamera} />
|
<CardHeader title="Front Overview" icon={faCamera} />
|
||||||
<SightingOverview />
|
<SightingOverview />
|
||||||
{/* <SnapshotContainer side="TargetDetectionFront" /> */}
|
{/* <SnapshotContainer side="TargetDetectionFront" /> */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const RearCameraOverviewCard = ({ className }: CardProps) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
||||||
<CardHeader title="Rear Overiew" icon={faCamera} />
|
<CardHeader title="Rear Overview" icon={faCamera} />
|
||||||
<SightingOverview />
|
<SightingOverview />
|
||||||
{/* <SnapshotContainer side="TargetDetectionRear" /> */}
|
{/* <SnapshotContainer side="TargetDetectionRear" /> */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const SightingOverview = () => {
|
|||||||
<div className="mt-2 grid gap-3">
|
<div className="mt-2 grid gap-3">
|
||||||
<div className="inline-block w-[90%] mx-auto" {...handlers}>
|
<div className="inline-block w-[90%] mx-auto" {...handlers}>
|
||||||
<NavigationArrow side={side} />
|
<NavigationArrow side={side} />
|
||||||
<div className="relative aspect-[5/4]">
|
<div className="relative aspect-[1280/800]">
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ const SightingWidgetDetails = ({
|
|||||||
}: SightingWidgetDetailsProps) => {
|
}: SightingWidgetDetailsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
VRM:{" "}
|
||||||
|
<span className="opacity-90">{effectiveSelected?.vrm ?? "—"}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Timestamp:{" "}
|
||||||
|
<span className="opacity-90">{effectiveSelected?.timeStamp ?? "—"}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Make:{" "}
|
Make:{" "}
|
||||||
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
|
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
|
||||||
@@ -17,6 +25,16 @@ const SightingWidgetDetails = ({
|
|||||||
Model:{" "}
|
Model:{" "}
|
||||||
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
|
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
Country:{" "}
|
||||||
|
<span className="opacity-90">{effectiveSelected?.countryCode ?? "—"}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Seen:{" "}
|
||||||
|
<span className="opacity-90">
|
||||||
|
{effectiveSelected?.seenCount ?? "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Colour:{" "}
|
Colour:{" "}
|
||||||
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
|
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
|
||||||
@@ -43,40 +61,8 @@ const SightingWidgetDetails = ({
|
|||||||
{effectiveSelected?.overviewSize ?? "—"}
|
{effectiveSelected?.overviewSize ?? "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
Motion:{" "}
|
|
||||||
<span className="opacity-90">{effectiveSelected?.motion ?? "—"}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Seen:{" "}
|
|
||||||
<span className="opacity-90">
|
|
||||||
{effectiveSelected?.seenCount ?? "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Location:{" "}
|
|
||||||
<span className="opacity-90">
|
|
||||||
{effectiveSelected?.locationName ?? "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Lane:{" "}
|
|
||||||
<span className="opacity-90">{effectiveSelected?.laneID ?? "—"}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Radar:{" "}
|
|
||||||
<span className="opacity-90">
|
|
||||||
{effectiveSelected?.radarSpeed ?? "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Track:{" "}
|
|
||||||
<span className="opacity-90">
|
|
||||||
{effectiveSelected?.trackSpeed ?? "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{effectiveSelected?.detailsUrl ? (
|
{effectiveSelected?.detailsUrl ? (
|
||||||
<div className="col-span-full">
|
<div className="col-span-half">
|
||||||
<a
|
<a
|
||||||
href={effectiveSelected.detailsUrl}
|
href={effectiveSelected.detailsUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Card = ({ children, className }: CardProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-[#253445] rounded-lg mt-4 mx-4 px-4 py-4 shadow-2xl overflow-y-auto md:row-span-1",
|
"bg-[#253445] rounded-lg mt-4 mx-2 px-4 py-4 shadow-2xl overflow-y-auto md:row-span-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,60 @@
|
|||||||
|
import * as React from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import Logo from "/MAV.svg";
|
import Logo from "/MAV.svg";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faGear } from "@fortawesome/free-solid-svg-icons";
|
import { faGear, faListCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faListCheck } from "@fortawesome/free-solid-svg-icons";
|
import type { VersionFieldType } from "../../types/types";
|
||||||
|
|
||||||
|
async function fetchVersions(signal?: AbortSignal): Promise<VersionFieldType> {
|
||||||
|
const res = await fetch("http://192.168.75.11/api/versions", { signal });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
const normalizeToMs = (ts: number) => (ts < 1e12 ? ts * 1000 : ts); // seconds → ms if needed
|
||||||
|
|
||||||
|
function formatFromMs(ms: number, tz: "local" | "utc" = "local") {
|
||||||
|
const d = new Date(ms);
|
||||||
|
const h = tz === "utc" ? d.getUTCHours() : d.getHours();
|
||||||
|
const m = tz === "utc" ? d.getUTCMinutes() : d.getMinutes();
|
||||||
|
const s = tz === "utc" ? d.getUTCSeconds() : d.getSeconds();
|
||||||
|
const day = tz === "utc" ? d.getUTCDate() : d.getDate();
|
||||||
|
const month = (tz === "utc" ? d.getUTCMonth() : d.getMonth()) + 1;
|
||||||
|
const year = tz === "utc" ? d.getUTCFullYear() : d.getFullYear();
|
||||||
|
return `${pad(h)}:${pad(m)}:${pad(s)} ${pad(day)}-${pad(month)}-${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const [offsetMs, setOffsetMs] = React.useState<number | null>(null);
|
||||||
|
const [nowMs, setNowMs] = React.useState<number>(Date.now());
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const ac = new AbortController();
|
||||||
|
fetchVersions(ac.signal)
|
||||||
|
.then((data) => {
|
||||||
|
const serverMs = normalizeToMs(data.timeStamp);
|
||||||
|
setOffsetMs(serverMs - Date.now());
|
||||||
|
})
|
||||||
|
return () => ac.abort();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let timer: number;
|
||||||
|
const schedule = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
setNowMs(now);
|
||||||
|
const delay = 1000 - (now % 1000);
|
||||||
|
timer = window.setTimeout(schedule, delay);
|
||||||
|
};
|
||||||
|
schedule();
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const serverNowMs = offsetMs == null ? nowMs : nowMs + offsetMs;
|
||||||
|
const localStr = formatFromMs(serverNowMs, "local");
|
||||||
|
const utcStr = formatFromMs(serverNowMs, "utc");
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4 flex flex-row justify-between">
|
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4 flex flex-row justify-between">
|
||||||
{/* Left: Logo */}
|
{/* Left: Logo */}
|
||||||
@@ -13,17 +63,20 @@ const Header = () => {
|
|||||||
<img src={Logo} alt="Logo" width={150} height={150} />
|
<img src={Logo} alt="Logo" width={150} height={150} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-8">
|
{/* Right: Texts stacked + icons */}
|
||||||
|
<div className="flex items-center space-x-12">
|
||||||
|
<div className="flex flex-col leading-tight text-white text-sm tabular-nums">
|
||||||
|
<h2>Local: {localStr}</h2>
|
||||||
|
<h2>UTC: {utcStr}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link to={"/session-settings"}>
|
<Link to={"/session-settings"}>
|
||||||
<FontAwesomeIcon icon={faListCheck} />
|
<FontAwesomeIcon className="text-white" icon={faListCheck} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link to={"/system-settings"}>
|
<Link to={"/system-settings"}>
|
||||||
<FontAwesomeIcon icon={faGear} />
|
<FontAwesomeIcon className="text-white" icon={faGear} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
|
|||||||
if (settingsPage) {
|
if (settingsPage) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{side === "TargetDetectionFront" ? (
|
{side === "CameraFront" ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowRight}
|
icon={faArrowRight}
|
||||||
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
|
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function useGetOverviewSnapshot(cameraSide: string) {
|
|||||||
queryKey: ["overviewSnapshot", cameraSide],
|
queryKey: ["overviewSnapshot", cameraSide],
|
||||||
queryFn: () => fetchSnapshot(cameraSide),
|
queryFn: () => fetchSnapshot(cameraSide),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
// refetchInterval: 1000,
|
refetchInterval: 250,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,89 +1,70 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { SightingWidgetType } from "../types/types";
|
import type { SightingWidgetType } from "../types/types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
// const url = `http://100.82.205.44/SightingListFront/sightingSummary?mostRecentRef=-1`;
|
async function fetchSighting(url: string, ref: number): Promise<SightingWidgetType> {
|
||||||
|
const res = await fetch(`${url}${ref}`);
|
||||||
async function fetchSighting(url: string, ref: number, signal?: AbortSignal) {
|
|
||||||
const dynamicUrl = `${url}${ref}`;
|
|
||||||
|
|
||||||
const res = await fetch(dynamicUrl, { signal });
|
|
||||||
if (!res.ok) throw new Error(String(res.status));
|
if (!res.ok) throw new Error(String(res.status));
|
||||||
return (await res.json()) as SightingWidgetType;
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSightingFeed(url: string) {
|
export function useSightingFeed(url: string) {
|
||||||
const [sightings, setSightings] = useState<SightingWidgetType[]>(
|
const [sightings, setSightings] = useState<SightingWidgetType[]>([]);
|
||||||
() => Array(7).fill(null) as unknown as SightingWidgetType[]
|
|
||||||
);
|
|
||||||
const [noSighting, setNoSighting] = useState(false);
|
|
||||||
const [selectedRef, setSelectedRef] = useState<number | null>(null);
|
const [selectedRef, setSelectedRef] = useState<number | null>(null);
|
||||||
const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null);
|
const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null);
|
||||||
|
|
||||||
const mostRecentRef = useRef<number>(-1);
|
const currentRef = useRef<number>(-1);
|
||||||
|
const pollingTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const lastSeenRef = useRef<number | null>(null);
|
const lastValidTimestamp = useRef<number>(Date.now());
|
||||||
|
|
||||||
const { data, isPending } = useQuery({
|
|
||||||
queryKey: ["sighting"],
|
|
||||||
queryFn: ({ signal }) => fetchSighting(url, mostRecentRef.current, signal),
|
|
||||||
refetchInterval: 2000,
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
staleTime: 0,
|
|
||||||
notifyOnChangeProps: ["data"],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchSighting(url, currentRef.current);
|
||||||
|
|
||||||
if (data.ref === -1) {
|
const now = Date.now();
|
||||||
setNoSighting(true);
|
|
||||||
} else {
|
|
||||||
setNoSighting(false);
|
|
||||||
}
|
|
||||||
if (data.ref === lastSeenRef.current) return; // duplicate payload → do nothing
|
|
||||||
lastSeenRef.current = data.ref;
|
|
||||||
|
|
||||||
setMostRecent(data);
|
if (data.ref === -1) {
|
||||||
|
if (now - lastValidTimestamp.current > 60000) {
|
||||||
|
console.warn("No valid sighting in over a minute. Restarting...");
|
||||||
|
currentRef.current = -1;
|
||||||
|
lastValidTimestamp.current = now;
|
||||||
|
}
|
||||||
|
|
||||||
setSightings((prev) => {
|
pollingTimeout.current = setTimeout(poll, 400);
|
||||||
const existing = prev.find((p) => p?.ref === data.ref);
|
} else {
|
||||||
const next = existing
|
currentRef.current = data.ref;
|
||||||
? prev
|
lastValidTimestamp.current = now;
|
||||||
: [data, ...prev.filter(Boolean)].slice(0, 7);
|
|
||||||
|
|
||||||
const stillHasSelection =
|
setSightings(prev => {
|
||||||
selectedRef != null && next.some((s) => s?.ref === selectedRef);
|
const updated = [data, ...prev].slice(0, 7);
|
||||||
if (!stillHasSelection) {
|
return updated;
|
||||||
setSelectedRef(data.ref);
|
});
|
||||||
|
|
||||||
|
setMostRecent(data);
|
||||||
|
setSelectedRef(data.ref);
|
||||||
|
|
||||||
|
pollingTimeout.current = setTimeout(poll, 100);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Polling error:", err);
|
||||||
|
pollingTimeout.current = setTimeout(poll, 100);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return next;
|
poll();
|
||||||
});
|
|
||||||
// setMostRecent(sightings[0]);
|
|
||||||
// setMostRecent(data);
|
|
||||||
mostRecentRef.current = data.ref ?? -1;
|
|
||||||
}, [data, selectedRef, sightings]);
|
|
||||||
|
|
||||||
const selected = useMemo(
|
return () => {
|
||||||
() =>
|
if (pollingTimeout.current) clearTimeout(pollingTimeout.current);
|
||||||
selectedRef == null
|
};
|
||||||
? null
|
}, [url]);
|
||||||
: sightings.find((s) => s?.ref === selectedRef) ?? null,
|
|
||||||
[sightings, selectedRef]
|
|
||||||
);
|
|
||||||
|
|
||||||
const effectiveSelected = selected ?? mostRecent ?? null;
|
const selected = sightings.find(s => s?.ref === selectedRef) ?? mostRecent;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sightings,
|
sightings,
|
||||||
selectedRef,
|
selectedRef,
|
||||||
setSelectedRef,
|
setSelectedRef,
|
||||||
mostRecent,
|
mostRecent,
|
||||||
effectiveSelected,
|
effectiveSelected: selected,
|
||||||
mostRecentRef,
|
|
||||||
isPending,
|
|
||||||
noSighting,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const FrontCamera = () => {
|
|||||||
>
|
>
|
||||||
<OverviewVideoContainer
|
<OverviewVideoContainer
|
||||||
title={"Front Camera"}
|
title={"Front Camera"}
|
||||||
side="TargetDetectionFront"
|
side="CameraFront"
|
||||||
settingsPage={true}
|
settingsPage={true}
|
||||||
/>
|
/>
|
||||||
<CameraSettings title="Front Camera Settings" />
|
<CameraSettings title="Front Camera Settings" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const RearCamera = () => {
|
|||||||
<CameraSettings title="Rear Camera Settings" />
|
<CameraSettings title="Rear Camera Settings" />
|
||||||
<OverviewVideoContainer
|
<OverviewVideoContainer
|
||||||
title={"Rear Camera"}
|
title={"Rear Camera"}
|
||||||
side={"TargetDetectionRear"}
|
side={"CameraRear"}
|
||||||
settingsPage={true}
|
settingsPage={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -102,6 +102,18 @@ export type SightingWidgetType = {
|
|||||||
// list of rects normalized 0..1
|
// list of rects normalized 0..1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VersionFieldType = {
|
||||||
|
version: string;
|
||||||
|
revision: string;
|
||||||
|
buildtime: string;
|
||||||
|
MAC: string;
|
||||||
|
timeStamp: number;
|
||||||
|
UUID: string;
|
||||||
|
"Serial No.": string;
|
||||||
|
"Model No.": string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type Metadata = {
|
export type Metadata = {
|
||||||
npedJSON: NpedJSON;
|
npedJSON: NpedJSON;
|
||||||
"hotlist-matches": HotlistMatches;
|
"hotlist-matches": HotlistMatches;
|
||||||
|
|||||||
Reference in New Issue
Block a user