diff --git a/web/src/components/overlay/ObjectTrackOverlay.tsx b/web/src/components/overlay/ObjectTrackOverlay.tsx index 593dc0b1f..25f9e12c4 100644 --- a/web/src/components/overlay/ObjectTrackOverlay.tsx +++ b/web/src/components/overlay/ObjectTrackOverlay.tsx @@ -103,6 +103,7 @@ export default function ObjectTrackOverlay({ })); }, [config, camera, getZoneColor, currentObjectZones]); + // get saved path points from event const savedPathPoints = useMemo(() => { return ( eventData?.[0].data?.path_data?.map( @@ -116,6 +117,7 @@ export default function ObjectTrackOverlay({ ); }, [eventData]); + // timeline points for selected event const eventSequencePoints = useMemo(() => { return ( objectTimeline @@ -124,8 +126,8 @@ export default function ObjectTrackOverlay({ const [left, top, width, height] = event.data.box!; return { - x: left + width / 2, // Center x-coordinate - y: top + height, // Bottom y-coordinate + x: left + width / 2, // Center x + y: top + height, // Bottom y timestamp: event.timestamp, lifecycle_item: event, }; @@ -152,6 +154,7 @@ export default function ObjectTrackOverlay({ ); }, [savedPathPoints, eventSequencePoints, config, camera, currentTime]); + // get absolute positions on the svg canvas for each point const getAbsolutePositions = useCallback(() => { if (!pathPoints) return []; return pathPoints.map((point) => { @@ -165,7 +168,7 @@ export default function ObjectTrackOverlay({ timestamp: point.timestamp, lifecycle_item: timelineEntry || - (point.box + (point.box // normal path point ? { timestamp: point.timestamp, camera: camera, @@ -220,22 +223,88 @@ export default function ObjectTrackOverlay({ [typeColorMap], ); + const handlePointClick = useCallback( + (timestamp: number) => { + onSeekToTime?.(timestamp); + }, + [onSeekToTime], + ); + + // render bounding box for object at current time if we have a timeline entry + const currentBoundingBox = useMemo(() => { + if (!objectTimeline) return null; + + // Find the most recent timeline event at or before effective current time with a bounding box + const relevantEvents = objectTimeline + .filter( + (event) => event.timestamp <= effectiveCurrentTime && event.data.box, + ) + .sort((a, b) => b.timestamp - a.timestamp); // Most recent first + + const currentEvent = relevantEvents[0]; + + if (!currentEvent?.data.box) return null; + + const [left, top, width, height] = currentEvent.data.box; + return { + left, + top, + width, + height, + centerX: left + width / 2, + centerY: top + height, + }; + }, [objectTimeline, effectiveCurrentTime]); + + const objectColor = useMemo(() => { + return pathPoints[0]?.label + ? getObjectColor(pathPoints[0].label) + : "rgb(255, 0, 0)"; + }, [pathPoints, getObjectColor]); + + const objectColorArray = useMemo(() => { + return pathPoints[0]?.label + ? getObjectColor(pathPoints[0].label).match(/\d+/g)?.map(Number) || [ + 255, 0, 0, + ] + : [255, 0, 0]; + }, [pathPoints, getObjectColor]); + + const absolutePositions = useMemo( + () => getAbsolutePositions(), + [getAbsolutePositions], + ); + + // render any zones for object at current time + const zonePolygons = useMemo(() => { + return zones.map((zone) => { + // Convert zone coordinates from normalized (0-1) to pixel coordinates + const points = zone.coordinates + .split(",") + .map(Number.parseFloat) + .reduce((acc: string[], value, index) => { + const isXCoordinate = index % 2 === 0; + const coordinate = isXCoordinate + ? value * videoWidth + : value * videoHeight; + acc.push(coordinate.toString()); + return acc; + }, []) + .join(","); + + return { + key: zone.name, + points, + fill: `rgba(${zone.color.replace("rgb(", "").replace(")", "")}, 0.3)`, + stroke: zone.color, + }; + }); + }, [zones, videoWidth, videoHeight]); + if (!pathPoints.length || !config) { return null; } - // Get the object color from the first point's label - const objectColor = pathPoints[0]?.label - ? getObjectColor(pathPoints[0].label) - : "rgb(255, 0, 0)"; - const objectColorArray = pathPoints[0]?.label - ? getObjectColor(pathPoints[0].label).match(/\d+/g)?.map(Number) || [ - 255, 0, 0, - ] - : [255, 0, 0]; - - const absolutePositions = getAbsolutePositions(); - return ( - {/* Render zones */} - {zones.map((zone) => { - // Convert zone coordinates from normalized (0-1) to pixel coordinates - const points = zone.coordinates - .split(",") - .map(Number.parseFloat) - .reduce((acc: string[], value, index) => { - const isXCoordinate = index % 2 === 0; - const coordinate = isXCoordinate - ? value * videoWidth - : value * videoHeight; - acc.push(coordinate.toString()); - return acc; - }, []) - .join(","); + {zonePolygons.map((zone) => ( + + ))} - return ( - - ); - })} - - {/* Draw path connecting the points */} {absolutePositions.length > 1 && ( )} - {/* Draw points with tooltips */} {absolutePositions.map((pos, index) => ( @@ -301,9 +351,7 @@ export default function ObjectTrackOverlay({ stroke="white" strokeWidth="3" style={{ cursor: onSeekToTime ? "pointer" : "default" }} - onClick={() => { - onSeekToTime?.(pos.timestamp); - }} + onClick={() => handlePointClick(pos.timestamp)} /> @@ -321,53 +369,30 @@ export default function ObjectTrackOverlay({ ))} - {/* Highlight current position with bounding box */} - {(() => { - if (!objectTimeline) return null; + {currentBoundingBox && ( + + - // Find the most recent timeline event at or before effective current time with a bounding box - const relevantEvents = objectTimeline - .filter( - (event) => - event.timestamp <= effectiveCurrentTime && event.data.box, - ) - .sort((a, b) => b.timestamp - a.timestamp); // Most recent first - - const currentEvent = relevantEvents[0]; - - if (currentEvent && currentEvent.data.box) { - const [left, top, width, height] = currentEvent.data.box; - const centerX = left + width / 2; - const centerY = top + height; - - return ( - - {/* Bounding box */} - - {/* Center point highlight */} - - - ); - } - return null; - })()} + + + )} ); }