From a220678321e3ffd243caa98bd19046d05b5373c0 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:17:48 -0500 Subject: [PATCH] match object lifecycle with details --- .../overlay/detail/ObjectLifecycle.tsx | 383 +++++++++++------- 1 file changed, 239 insertions(+), 144 deletions(-) diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx index d335e35c9..0f1eaadf5 100644 --- a/web/src/components/overlay/detail/ObjectLifecycle.tsx +++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx @@ -47,6 +47,7 @@ import { getLifecycleItemDescription } from "@/utils/lifecycleUtil"; import { IoPlayCircleOutline } from "react-icons/io5"; import { useTranslation } from "react-i18next"; import { getTranslatedLabel } from "@/utils/i18n"; +import { Badge } from "@/components/ui/badge"; type ObjectLifecycleProps = { className?: string; @@ -355,6 +356,52 @@ export default function ObjectLifecycle({ return idx === -1 ? 0 : idx; }, [eventSequence, timeIndex]); + // Calculate how far down the blue line should extend based on timeIndex + const calculateLineHeight = () => { + if (!eventSequence || eventSequence.length === 0) return 0; + + const currentTime = timeIndex ?? 0; + + // Find which events have been passed + let lastPassedIndex = -1; + for (let i = 0; i < eventSequence.length; i++) { + if (currentTime >= (eventSequence[i].timestamp ?? 0)) { + lastPassedIndex = i; + } else { + break; + } + } + + // No events passed yet + if (lastPassedIndex < 0) return 0; + + // All events passed + if (lastPassedIndex >= eventSequence.length - 1) return 100; + + // Calculate percentage based on item position, not time + // Each item occupies an equal visual space regardless of time gaps + const itemPercentage = 100 / (eventSequence.length - 1); + + // Find progress between current and next event for smooth transition + const currentEvent = eventSequence[lastPassedIndex]; + const nextEvent = eventSequence[lastPassedIndex + 1]; + const currentTimestamp = currentEvent.timestamp ?? 0; + const nextTimestamp = nextEvent.timestamp ?? 0; + + // Calculate interpolation between the two events + const timeBetween = nextTimestamp - currentTimestamp; + const timeElapsed = currentTime - currentTimestamp; + const interpolation = timeBetween > 0 ? timeElapsed / timeBetween : 0; + + // Base position plus interpolated progress to next item + return Math.min( + 100, + lastPassedIndex * itemPercentage + interpolation * itemPercentage, + ); + }; + + const blueLineHeight = calculateLineHeight(); + if (!config) { return ; } @@ -569,7 +616,7 @@ export default function ObjectLifecycle({
@@ -581,10 +628,12 @@ export default function ObjectLifecycle({ }} role="button" > - {getIconForLabel( - event.label, - "size-6 text-primary dark:text-white", - )} +
+ {getIconForLabel( + event.label, + "size-6 text-primary dark:text-white", + )} +
{getTranslatedLabel(event.label)} @@ -602,147 +651,79 @@ export default function ObjectLifecycle({ {t("detail.noObjectDetailData", { ns: "views/events" })}
) : ( -
- {eventSequence.map((item, idx) => { - const isActive = - Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5; - const formattedEventTimestamp = config - ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { - timezone: config.ui.timezone, - date_format: - config.ui.time_format == "24hour" - ? t( - "time.formattedTimestampHourMinuteSecond.24hour", - { ns: "common" }, - ) - : t( - "time.formattedTimestampHourMinuteSecond.12hour", - { ns: "common" }, - ), - time_style: "medium", - date_style: "medium", - }) - : ""; +
+
+
+
+ {eventSequence.map((item, idx) => { + const isActive = + Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5; + const formattedEventTimestamp = config + ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { + timezone: config.ui.timezone, + date_format: + config.ui.time_format == "24hour" + ? t( + "time.formattedTimestampHourMinuteSecond.24hour", + { ns: "common" }, + ) + : t( + "time.formattedTimestampHourMinuteSecond.12hour", + { ns: "common" }, + ), + time_style: "medium", + date_style: "medium", + }) + : ""; - const ratio = - Array.isArray(item.data.box) && item.data.box.length >= 4 - ? ( - aspectRatio * - (item.data.box[2] / item.data.box[3]) - ).toFixed(2) - : "N/A"; - const areaPx = - Array.isArray(item.data.box) && item.data.box.length >= 4 - ? Math.round( - (config.cameras[event.camera]?.detect?.width ?? 0) * - (config.cameras[event.camera]?.detect?.height ?? - 0) * - (item.data.box[2] * item.data.box[3]), - ) - : undefined; - const areaPct = - Array.isArray(item.data.box) && item.data.box.length >= 4 - ? (item.data.box[2] * item.data.box[3]).toFixed(4) - : undefined; + const ratio = + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? ( + aspectRatio * + (item.data.box[2] / item.data.box[3]) + ).toFixed(2) + : "N/A"; + const areaPx = + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? Math.round( + (config.cameras[event.camera]?.detect?.width ?? 0) * + (config.cameras[event.camera]?.detect?.height ?? + 0) * + (item.data.box[2] * item.data.box[3]), + ) + : undefined; + const areaPct = + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? (item.data.box[2] * item.data.box[3]).toFixed(4) + : undefined; - return ( -
{ - setTimeIndex(item.timestamp ?? 0); - handleSetBox( - item.data.box ?? [], - item.data.attribute_box, - ); - setLifecycleZones(item.data.zones); - setSelectedZone(""); - }} - className={cn( - "flex cursor-pointer flex-col gap-1 rounded-md p-2 text-sm text-primary-variant", - isActive - ? "bg-secondary-highlight font-semibold text-primary outline-[1.5px] -outline-offset-[1.1px] outline-primary/40 dark:font-normal" - : "duration-500", - )} - > -
-
- -
-
-
{getLifecycleItemDescription(item)}
-
- {formattedEventTimestamp} -
-
-
- -
-
-
- - {t( - "objectLifecycle.lifecycleItemDesc.header.ratio", - )} - - - {ratio} - -
- -
- - {t( - "objectLifecycle.lifecycleItemDesc.header.area", - )} - - {areaPx !== undefined && areaPct !== undefined ? ( - - px: {areaPx} · %: {areaPct} - - ) : ( - N/A - )} -
- {item.class_type === "entered_zone" && ( -
- - {t( - "objectLifecycle.lifecycleItemDesc.header.zones", - )} - -
- {item.data.zones.map((zone, zidx) => ( -
{ - e.stopPropagation(); - setSelectedZone(zone); - }} - > -
- - {zone.replaceAll("_", " ")} - -
- ))} -
-
- )} -
-
-
- ); - })} + return ( + { + setTimeIndex(item.timestamp ?? 0); + handleSetBox( + item.data.box ?? [], + item.data.attribute_box, + ); + setLifecycleZones(item.data.zones); + setSelectedZone(""); + }} + setSelectedZone={setSelectedZone} + getZoneColor={getZoneColor} + /> + ); + })} +
)}
@@ -789,3 +770,117 @@ export function LifecycleIcon({ return null; } } + +type LifecycleIconRowProps = { + item: ObjectLifecycleSequence; + isActive?: boolean; + formattedEventTimestamp: string; + ratio: string; + areaPx?: number; + areaPct?: string; + onClick: () => void; + setSelectedZone: (z: string) => void; + getZoneColor: (zoneName: string) => number[] | undefined; +}; + +function LifecycleIconRow({ + item, + isActive, + formattedEventTimestamp, + ratio, + areaPx, + areaPct, + onClick, + setSelectedZone, + getZoneColor, +}: LifecycleIconRowProps) { + const { t } = useTranslation(["views/explore"]); + + return ( +
+
+
+ +
+ +
+
+
{getLifecycleItemDescription(item)}
+
+
+ + {t("objectLifecycle.lifecycleItemDesc.header.ratio")} + + {ratio} +
+
+ + {t("objectLifecycle.lifecycleItemDesc.header.area")} + + {areaPx !== undefined && areaPct !== undefined ? ( + + {t("information.pixels", { ns: "common", area: areaPx })} ·{" "} + {areaPct}% + + ) : ( + N/A + )} +
+ + {item.data?.zones && item.data.zones.length > 0 && ( +
+ {item.data.zones.map((zone, zidx) => { + const color = getZoneColor(zone)?.join(",") ?? "0,0,0"; + return ( + { + e.stopPropagation(); + setSelectedZone(zone); + }} + style={{ + borderColor: `rgba(${color}, 0.6)`, + background: `rgba(${color}, 0.08)`, + }} + > + + + {zone.replaceAll("_", " ")} + + + ); + })} +
+ )} +
+
+ +
{formattedEventTimestamp}
+
+
+
+ ); +}