added sighting feed
This commit is contained in:
@@ -15,7 +15,7 @@ export const SnapshotContainer = ({
|
||||
return (
|
||||
<div className="relative w-full aspect-video">
|
||||
<NavigationArrow side={side} settingsPage={settingsPage} />
|
||||
<canvas ref={canvasRef} className="w-full h-full object-contain block " />
|
||||
<canvas ref={canvasRef} className="w-full h-full object-contain block" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@ import clsx from "clsx";
|
||||
import Card from "../UI/Card";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
import { faCamera } from "@fortawesome/free-regular-svg-icons";
|
||||
import { SnapshotContainer } from "../CameraOverview/SnapshotContainer";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useOverviewVideo } from "../../hooks/useOverviewVideo";
|
||||
import SightingOverview from "../SightingOverview/SightingOverview";
|
||||
|
||||
const FrontCameraOverviewCard = () => {
|
||||
type CardProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const FrontCameraOverviewCard = ({ className }: CardProps) => {
|
||||
useOverviewVideo();
|
||||
const navigate = useNavigate();
|
||||
const handlers = useSwipeable({
|
||||
@@ -16,10 +18,16 @@ const FrontCameraOverviewCard = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className={clsx("relative min-h-[40vh] md:min-h-[60vh] h-auto")}>
|
||||
<Card
|
||||
className={clsx(
|
||||
"relative min-h-[40vh] md:min-h-[60vh] h-auto",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
||||
<CardHeader title="Front Overiew" icon={faCamera} />
|
||||
<SnapshotContainer side="TargetDetectionFront" />
|
||||
<SightingOverview />
|
||||
{/* <SnapshotContainer side="TargetDetectionFront" /> */}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import Card from "../UI/Card";
|
||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
|
||||
const Output = () => {
|
||||
return (
|
||||
<Card className={clsx("min-h-[40vh] md:min-h-[60vh] h-auto")}>
|
||||
<CardHeader title="Output" icon={faSliders} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Output;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Formik, Form, Field } from "formik";
|
||||
|
||||
const OutputForm = () => {
|
||||
const initialValues = {
|
||||
includeVRM: false,
|
||||
includeMotion: false,
|
||||
includeTimestamp: false,
|
||||
timeStampFormat: "utc",
|
||||
};
|
||||
return <div>OutputForm</div>;
|
||||
};
|
||||
|
||||
export default OutputForm;
|
||||
@@ -1,17 +1,17 @@
|
||||
import { GB } from "country-flag-icons/react/3x2";
|
||||
import { formatNumberPlate } from "../../utils/utils";
|
||||
import type { SightingType } from "../../types/types";
|
||||
|
||||
type NumberPlateProps = {
|
||||
sighting: SightingType;
|
||||
vrm?: string | undefined;
|
||||
motion?: boolean;
|
||||
};
|
||||
|
||||
const NumberPlate = ({ sighting }: NumberPlateProps) => {
|
||||
const NumberPlate = ({ motion, vrm }: NumberPlateProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`relative w-[8rem] border-4 border-black rounded-lg text-nowrap
|
||||
text-black px-3
|
||||
${sighting?.motion !== "towards" ? "bg-yellow-400" : "bg-white"}
|
||||
${motion ? "bg-yellow-400" : "bg-white"}
|
||||
`}
|
||||
>
|
||||
<div className="">
|
||||
@@ -19,7 +19,7 @@ const NumberPlate = ({ sighting }: NumberPlateProps) => {
|
||||
<GB />
|
||||
</div>
|
||||
<p className=" pl-2 font-extrabold text-right">
|
||||
{formatNumberPlate(sighting?.vrm)}
|
||||
{vrm && formatNumberPlate(vrm)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import NumberPlate from "./NumberPlate";
|
||||
import SightingCanvas from "../SightingOverview/SightingCanvas";
|
||||
|
||||
import type { SightingType } from "../../types/types";
|
||||
|
||||
type SightingProps = {
|
||||
@@ -9,9 +9,8 @@ type SightingProps = {
|
||||
const Sighting = ({ sighting }: SightingProps) => {
|
||||
return (
|
||||
<div className="bg-gray-700 flex flex-col md:flex-row m-1 items-center justify-between w-full rounded-md p-4 space-y-4">
|
||||
<SightingCanvas />
|
||||
<div className="flex flex-row m-1 items-center space-x-4">
|
||||
<NumberPlate sighting={sighting} />
|
||||
<NumberPlate />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,9 @@ import { useNavigate } from "react-router";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
import { faCamera } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const RearCameraOverviewCard = () => {
|
||||
type CardProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const RearCameraOverviewCard = ({ className }: CardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const handlers = useSwipeable({
|
||||
onSwipedLeft: () => navigate("/rear-camera-settings"),
|
||||
@@ -14,7 +16,7 @@ const RearCameraOverviewCard = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className={clsx("min-h-[40vh] md:min-h-[60vh] h-auto")}>
|
||||
<Card className={clsx("min-h-[40vh] md:min-h-[60vh] h-auto", className)}>
|
||||
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
||||
<CardHeader title="Rear Overiew" icon={faCamera} />
|
||||
<SnapshotContainer side="TargetDetectionRear" />
|
||||
|
||||
70
src/components/SightingOverview/SightingOverview.tsx
Normal file
70
src/components/SightingOverview/SightingOverview.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { BLANK_IMG } from "../../utils/utils";
|
||||
import SightingWidgetDetails from "../SightingsWidget/SightingWidgetDetails";
|
||||
import { useOverviewOverlay } from "../../hooks/useOverviewOverlay";
|
||||
import { useSightingFeedContext } from "../../context/SightingFeedContext";
|
||||
import { useHiDPICanvas } from "../../hooks/useHiDPICanvas";
|
||||
import NavigationArrow from "../UI/NavigationArrow";
|
||||
|
||||
const SightingOverview = () => {
|
||||
const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0);
|
||||
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const onOverviewClick = useCallback(() => {
|
||||
setOverlayMode((m) => ((m + 1) % 3) as 0 | 1 | 2);
|
||||
}, []);
|
||||
|
||||
const { effectiveSelected } = useSightingFeedContext();
|
||||
|
||||
useOverviewOverlay(effectiveSelected, overlayMode, imgRef, canvasRef);
|
||||
|
||||
const { sync } = useHiDPICanvas(imgRef, canvasRef);
|
||||
return (
|
||||
<div className="mt-2 grid gap-3">
|
||||
{/* <div className="flex items-center gap-3 text-sm">
|
||||
<div className="font-semibold">{effectiveSelected?.vrm ?? "—"}</div>
|
||||
<div>{effectiveSelected?.countryCode ?? "—"}</div>
|
||||
<div className="opacity-80">{effectiveSelected?.timeStamp ?? "—"}</div>
|
||||
</div> */}
|
||||
|
||||
<div className="inline-block">
|
||||
<div className="relative aspect-[5/4]">
|
||||
<img
|
||||
ref={imgRef}
|
||||
onLoad={() => {
|
||||
sync();
|
||||
setOverlayMode((m) => m);
|
||||
}}
|
||||
src={effectiveSelected?.overviewUrl || BLANK_IMG}
|
||||
alt="overview"
|
||||
className="absolute inset-0 w-full h-full object-contain cursor-pointer z-10"
|
||||
onClick={onOverviewClick}
|
||||
style={{
|
||||
display: effectiveSelected?.overviewUrl ? "block" : "none",
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 w-full h-full object-contain z-20 pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SightingWidgetDetails effectiveSelected={effectiveSelected} />
|
||||
|
||||
<div className="text-xs opacity-80">
|
||||
Overlay:{" "}
|
||||
{overlayMode === 0
|
||||
? "Off"
|
||||
: overlayMode === 1
|
||||
? "Plate box"
|
||||
: "Track + box"}{" "}
|
||||
(click image to toggle)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SightingOverview;
|
||||
121
src/components/SightingsWidget/SightingWidget.tsx
Normal file
121
src/components/SightingsWidget/SightingWidget.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { SightingWidgetType } from "../../types/types";
|
||||
import { BLANK_IMG, capitalize, formatAge } from "../../utils/utils";
|
||||
import NumberPlate from "../PlateStack/NumberPlate";
|
||||
import Card from "../UI/Card";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
import clsx from "clsx";
|
||||
import { useSightingFeedContext } from "../../context/SightingFeedContext";
|
||||
|
||||
function useNow(tickMs = 1000) {
|
||||
const [, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), tickMs);
|
||||
return () => clearInterval(id);
|
||||
}, [tickMs]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type SightingHistoryProps = {
|
||||
baseUrl: string;
|
||||
entries?: number; // number of rows to show
|
||||
pollMs?: number; // poll frequency
|
||||
autoSelectLatest?: boolean;
|
||||
};
|
||||
|
||||
type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export default function SightingHistoryWidget({
|
||||
className,
|
||||
}: SightingHistoryWidgetProps) {
|
||||
useNow(1000);
|
||||
const { items, selectedRef, setSelectedRef } = useSightingFeedContext();
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(ref: number) => {
|
||||
setSelectedRef(ref);
|
||||
},
|
||||
[setSelectedRef]
|
||||
);
|
||||
|
||||
const rows = useMemo(
|
||||
() => items.filter(Boolean) as SightingWidgetType[],
|
||||
[items]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={clsx("overflow-y-auto h-100", className)}>
|
||||
<CardHeader title="Front Camera Sightings" />
|
||||
<div className="flex flex-col gap-3 ">
|
||||
{/* Rows */}
|
||||
<div className="flex flex-col">
|
||||
{rows.map((obj, idx) => {
|
||||
const isSelected = obj?.ref === selectedRef;
|
||||
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
|
||||
const primaryIsColour = obj?.srcCam === 1;
|
||||
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer ${
|
||||
isSelected ? "ring-2 ring-blue-400" : ""
|
||||
}`}
|
||||
onClick={() => onRowClick(obj.ref)}
|
||||
>
|
||||
{/* Info bar */}
|
||||
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
|
||||
<div className="min-w-14">
|
||||
CH: {obj ? obj.charHeight : "—"}
|
||||
</div>
|
||||
<div className="min-w-14">
|
||||
Seen: {obj ? obj.seenCount : "—"}
|
||||
</div>
|
||||
<div className="min-w-20">
|
||||
{obj ? capitalize(obj.motion) : "—"}
|
||||
</div>
|
||||
<div className="min-w-14 opacity-80">
|
||||
{obj ? formatAge(obj.timeStampMillis) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Patch row */}
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<div
|
||||
className={`border p-1 ${
|
||||
primaryIsColour ? "" : "ring-2 ring-lime-400"
|
||||
} ${!obj ? "opacity-30" : ""}`}
|
||||
>
|
||||
<img
|
||||
src={obj?.plateUrlInfrared || BLANK_IMG}
|
||||
height={48}
|
||||
alt="infrared patch"
|
||||
className={!primaryIsColour ? "" : "opacity-60"}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`border p-1 ${
|
||||
primaryIsColour ? "ring-2 ring-lime-400" : ""
|
||||
} ${
|
||||
secondaryMissing && primaryIsColour
|
||||
? "opacity-30 grayscale"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={obj?.plateUrlColour || BLANK_IMG}
|
||||
height={48}
|
||||
alt="colour patch"
|
||||
className={primaryIsColour ? "" : "opacity-60"}
|
||||
/>
|
||||
</div>
|
||||
<NumberPlate motion={motionAway} vrm={obj?.vrm} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
93
src/components/SightingsWidget/SightingWidgetDetails.tsx
Normal file
93
src/components/SightingsWidget/SightingWidgetDetails.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { SightingWidgetType } from "../../types/types";
|
||||
|
||||
type SightingWidgetDetailsProps = {
|
||||
effectiveSelected: SightingWidgetType | null;
|
||||
};
|
||||
|
||||
const SightingWidgetDetails = ({
|
||||
effectiveSelected,
|
||||
}: SightingWidgetDetailsProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
||||
<div>
|
||||
Make:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Model:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Colour:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Category:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.category ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Char Ht:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.charHeight ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Plate Size:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.plateSize ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Overview Size:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.overviewSize ?? "—"}
|
||||
</span>
|
||||
</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 ? (
|
||||
<div className="col-span-full">
|
||||
<a
|
||||
href={effectiveSelected.detailsUrl}
|
||||
target="_blank"
|
||||
className="underline text-blue-300"
|
||||
>
|
||||
Sighting Details
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SightingWidgetDetails;
|
||||
Reference in New Issue
Block a user