Compare commits

...

95 Commits

Author SHA1 Message Date
RUSHIL AMBARISH KADU 7f2a096444 Checking correct origin tracking 1 week ago
RUSHIL AMBARISH KADU b9d2cbe84f Testing the new commit architecture on GITEA 1 week ago
RUSHIL AMBARISH KADU 639a926f95 fix(ui): restore missing resize handles for data explorer panel 4 weeks ago
RUSHIL AMBARISH KADU bc17f57b81 feat(explorer): implement vertical ADAS property view and optimize workspace layout 4 weeks ago
RUSHIL AMBARISH KADU eb4fd856c0 updated readme for readability, & updated gemini.md for AI readability. 2 months ago
RUSHIL AMBARISH KADU 8bffad0abc docs: restore original README content 2 months ago
RUSHIL AMBARISH KADU 82c9862ec2 Updating the intelligence. 2 months ago
RUSHIL AMBARISH KADU ef6480621b refactor(ui): Unified Floating Panel Engine & Persistent Dashboard Memory 2 months ago
RUSHIL AMBARISH KADU e57f3abaed refactor(ui): Decoupled floating panels from gridstack with persistent UX 2 months ago
RUSHIL AMBARISH KADU a819a72e01 Removed duplicated source of naming the zoom sketch. 2 months ago
RUSHIL AMBARISH KADU 18a7c5640e feat(ui): Refactor Zoom Sketch to floating panel with auto-hide UX 2 months ago
RUSHIL AMBARISH KADU 4506cbc569 feat(offline): enable GridStack offline-first fallback and harden local launch scripts 2 months ago
RUSHIL AMBARISH KADU dc19e848d0 feat(layout): implement GridStack.js for resizable dashboards and fix p5.js resize stability 2 months ago
RUSHIL AMBARISH KADU ea2d367500 ARAS Visualizer Version 3.3.0 - Executive Summary 2 months ago
RUSHIL AMBARISH KADU 2b53a589a7 V3.3.0. Stable release: Implemented cache-busting server, robust filename regex, and global ESC-key modal dismiss. Added null-guards for video-only loading and log-cfg utility. 2 months ago
RUSHIL AMBARISH KADU 4cb40527ea **Release Draft: v3.2.7 (March 18, 2026)** 2 months ago
RUSHIL AMBARISH KADU 3d775be7b0 feat(ui): implement universal drag-and-drop and expanded start screen 2 months ago
RUSHIL AMBARISH KADU 2ebeaf0fab fix: resolve crashes during video-only file loading 2 months ago
RUSHIL AMBARISH KADU 30ef74be6e Tried to supress Tailwind CSS warning. 2 months ago
RUSHIL AMBARISH KADU 57d644e5e6 Ran command: `git diff` 2 months ago
RUSHIL AMBARISH KADU 1556231f17 feat: robust date-time parsing and persistent overlay resilience 2 months ago
RUSHIL AMBARISH KADU 4d8c03ffe2 feat(ui): redesign startup flow with Foxglove-style SPA loading screen 2 months ago
RUSHIL AMBARISH KADU d5f7ef02f7 perf(viz): optimize render pipeline and refine zoom camera UX 2 months ago
RUSHIL AMBARISH KADU 19576ccb49 feat(zoom): optimize rendering pipeline and polish camera UX 2 months ago
RUSHIL AMBARISH KADU c5c7069f74 style(overview): resize visual map and hide scrollbars 2 months ago
RUSHIL AMBARISH KADU 78fb576da6 Adding the root folder as well. 2 months ago
RUSHIL AMBARISH KADU aea83b2323 Added the tree structure to file system in code base overview. 2 months ago
RUSHIL AMBARISH KADU ecd521ee4d updating the intel 2 months ago
RUSHIL AMBARISH KADU 973471ca97 feat(ui): add changelog button and modal, repositioned to header end 2 months ago
RUSHIL AMBARISH KADU 3a12ca6765 chore: update internal paths for documentation moved to annex/ 2 months ago
RUSHIL AMBARISH KADU f5b4ebfa7a updated favicon, gitignore 2 months ago
RUSHIL AMBARISH KADU f70785bdad Updated the changelog. 3 months ago
RUSHIL AMBARISH KADU 27f582e9c9 Changelogs_Added 3 months ago
RUSHIL AMBARISH KADU b24f6f460e fix(viz): add stability guards for smoothing and data mapping 3 months ago
RUSHIL AMBARISH KADU 76d2fb1d47 feat(viz): implement frame-rate independent smoothing for UI and sketches 3 months ago
RUSHIL AMBARISH KADU b63b3ae294 * Fixed a bug in handleCloseUpDisplay where p.mouseY was being used directly instead of the passed-in (and potentially smoothed) mouseY value. 3 months ago
RUSHIL AMBARISH KADU 53aa70afe3 feat(viz): implement smooth mouse tracking for zoom tooltip 3 months ago
RUSHIL AMBARISH KADU 2391f3a889 feat(ui): improve IFT graph scaling animation and performance 3 months ago
RUSHIL AMBARISH KADU 086bef119d ✦ feat: enhance visualizations with smart labeling and density-based speed coloring 3 months ago
RUSHIL AMBARISH KADU ccdd91aee1 First iteration of GEmini.md file 3 months ago
RUSHIL AMBARISH KADU e7c72160ed - Add vertical range slider (20m-200m) to adjust RADAR_Y_MAX dynamically. 3 months ago
RUSHIL AMBARISH KADU 7aadf264d6 UX: Contraining the GodMode tooltip inside the bounding sketch box. 3 months ago
RUSHIL AMBARISH KADU 6a02df2e48 Inverse Zom Logic implemented. 3 months ago
RUSHIL AMBARISH KADU d26aaf0a25 Risk and State updated in track marker and zoom sketch tooltip 3 months ago
RUSHIL AMBARISH KADU 5ebd55d876 Confirmed ONLY tracks visible, now fixed 3 months ago
RUSHIL AMBARISH KADU 5a261a0391 Video_Player mute removed and controls option added. 4 months ago
RUSHIL AMBARISH KADU 193cb81599 Sun icon fixed. 4 months ago
RUSHIL AMBARISH KADU ff57a4fa1c minor Dark mode improvements here and there. 4 months ago
RUSHIL AMBARISH KADU b982a9179e 1 feat: add interactive codebase overview and architectural map 4 months ago
RUSHIL AMBARISH KADU 4a4e418e73 Readme.md updated once again. 4 months ago
RUSHIL AMBARISH KADU 90e171d34f feat: integrate codebase overview modal into main application 4 months ago
RUSHIL AMBARISH KADU c05f81aa98 feat: implement interactive codebase overview with visual navigation map 4 months ago
RUSHIL AMBARISH KADU 84f61ecd6e chore: add PrismJS dependencies for code syntax highlighting 4 months ago
RUSHIL AMBARISH KADU e13a734ab9 feat: Add vehicle dimensions visualization and enhance track risk coloring 4 months ago
RUSHIL AMBARISH KADU b905f8536b added state beside ttc marker on the visualization. 4 months ago
RUSHIL AMBARISH KADU 073f30c482 feat: Enhance radar overlay responsiveness and robustness 4 months ago
RUSHIL AMBARISH KADU bc239a552e New data structures added for reference. 4 months ago
RUSHIL AMBARISH KADU e4e07bb1c5 Auto loading quick start guide feature added. 5 months ago
RUSHIL AMBARISH KADU 5bab276c18 Margin reduction in user guide. 5 months ago
RUSHIL AMBARISH KADU e85b031f32 HTML changes. 5 months ago
RUSHIL AMBARISH KADU ffaf2a2209 feat: add user guide modal on first session launch 5 months ago
RUSHIL AMBARISH KADU 639ec34e5d Scale removed from Persistent overlay and theme button color scheme changed. 6 months ago
RUSHIL AMBARISH KADU 1652bb0658 feat(ui): add in-app modals for user guide and shortcuts 6 months ago
RUSHIL AMBARISH KADU f03bbbeeb0 feat(ui): add in-app modals for user guide and shortcuts. 6 months ago
RUSHIL AMBARISH KADU 0495bb084f refactor(app): Extract UI and session logic from main.js 6 months ago
RUSHIL AMBARISH KADU 29875de89f feat(ui): Enable timeline scroll on speed graph 6 months ago
RUSHIL AMBARISH KADU a6458d9de0 Fix minor issue. 6 months ago
RUSHIL AMBARISH KADU 9df5006f85 feat(offset): Improve manual offset workflow and persistence 6 months ago
RUSHIL AMBARISH KADU db3e7495c0 feat(file-loading): Improve FPS counter accuracy and video retention Enhance FPS counter stability by implementing a warmup period and explicit reset of appState.fps`, preventing erroneous spikes after new file loads. 6 months ago
RUSHIL AMBARISH KADU eed086b2bc Minor perf. boost by reducing text content updates. 6 months ago
RUSHIL AMBARISH KADU d79f7c51c8 The Fix: 6 months ago
RUSHIL AMBARISH KADU 8b667e6ad7 Throttling the update explorer for the MATLAB style data explorer had unnescessary function calls even when closed. Removed that for 20% perf. boost. 6 months ago
RUSHIL AMBARISH KADU e6543e39f1 DOM Caching and Inner HTML removal. 6 months ago
RUSHIL AMBARISH KADU b8bf85491e Perforamance boost by removing unnescessary calls like draw axes in every amimation loop 6 months ago
RUSHIL AMBARISH KADU bee80cda95 Forgot to add some more changes in the Inter frame timing Graph. 6 months ago
RUSHIL AMBARISH KADU c893ebf4b4 Added the Interframetiming Graph. 6 months ago
RUSHIL AMBARISH KADU 1b699018de Minor changes in Dyanic SNR name, Abs time removed from radar overlay (it was redundant) 6 months ago
RUSHIL AMBARISH KADU e5b5c6629e Updated context.md and Readme.md 6 months ago
RUSHIL AMBARISH KADU aa2c808e52 1 feat(viz): add interactive seeking to speed graph and optimize 6 months ago
RUSHIL AMBARISH KADU ae9b8dc62b refactor(core): Enhance robustness of file loading and UI state management 6 months ago
RUSHIL AMBARISH KADU 10c52d318a feat(loading): Implement robust video loading and non-blocking cache 6 months ago
RUSHIL AMBARISH KADU 59fef58ab4 *Goal* 6 months ago
RUSHIL AMBARISH KADU 79b611efe1 1 Refactor file loading architecture for modularity and robust synchronization 6 months ago
RUSHIL AMBARISH KADU cdaed0d4c6 File loading overhaul part 1 6 months ago
RUSHIL AMBARISH KADU 6d0535b8be Speed Graph Sketch issue solved using CLI 6 months ago
RUSHIL AMBARISH KADU 0980398ed2 Shift key god mode added. 6 months ago
RUSHIL AMBARISH KADU 5ee39dafac refactor(keyboard): Isolate keyboard shortcut logic 6 months ago
RUSHIL AMBARISH KADU 02601fd52c feat(sync): Rearchitect sync logic and add advanced navigation 6 months ago
RUSHIL AMBARISH KADU c1e30e3e17 Debug overlays fixed. 6 months ago
RUSHIL AMBARISH KADU 1adc8b78ec LEFT RIGHT UP DOWN Arrow keys shortcut added. 6 months ago
RUSHIL AMBARISH KADU 52ce3aa8f3 fix(sync): centralize offset handling and correct drift math 6 months ago
RUSHIL AMBARISH KADU 7690b899f5 feat(sync): hybrid architecture for video/radar synchronization 6 months ago
RUSHIL AMBARISH KADU a5c5400c57 Definitve fix of stutter issue. Now the playback engine is completely fixed and works as intended. 6 months ago
RUSHIL AMBARISH KADU cade96f1bd Update frame moved to videoframecallback. Now the Sync engine is 100% solid logic. 6 months ago
RUSHIL AMBARISH KADU 98c818365a Might be the solution an architectue change. 6 months ago
  1. 5
      .gitignore
  2. 69
      steps/Data_structs/JSON_Structure.json
  3. 67
      steps/Data_structs/JSON_Structure_trackHistory.json
  4. 89
      steps/Data_structs/JSON_Structure_v2.json
  5. 85
      steps/Data_structs/ROS2_Data_Structure.json
  6. 7
      steps/Data_structs/new.json
  7. 20
      steps/Visualization_Start.bat
  8. 214
      steps/annex/Changelog.html
  9. 118
      steps/annex/Changelog_3.3.0.html
  10. 20
      steps/annex/Changelog_V3.3.0.md
  11. BIN
      steps/annex/GIT_Changes.txt
  12. 25
      steps/annex/Improvements.txt
  13. 394
      steps/annex/User_Manual.html
  14. 1607
      steps/annex/code-base-overview.html
  15. 137
      steps/annex/shortcuts.html
  16. 84
      steps/context.md
  17. BIN
      steps/favicon.png
  18. 533
      steps/index.html
  19. 66
      steps/intel/GEMINI.md
  20. 143
      steps/intel/context.md
  21. 146
      steps/intel/readme.md
  22. 6
      steps/package-lock.json
  23. 230
      steps/readme.md
  24. 31
      steps/server.py
  25. 171
      steps/src/dataExplorer.js
  26. 148
      steps/src/db.js
  27. 24
      steps/src/debug.js
  28. 379
      steps/src/dom.js
  29. 453
      steps/src/drawUtils.js
  30. 508
      steps/src/fileLoader.js
  31. 40
      steps/src/fileParsers.js
  32. 201
      steps/src/keyboard.js
  33. 978
      steps/src/main.js
  34. 102
      steps/src/modal.js
  35. 318
      steps/src/p5/radarSketch.js
  36. 387
      steps/src/p5/speedGraphSketch.js
  37. 248
      steps/src/p5/zoomSketch.js
  38. 131
      steps/src/session.js
  39. 23
      steps/src/state.js
  40. 438
      steps/src/sync.js
  41. 36
      steps/src/theme.js
  42. 532
      steps/src/ui.js
  43. 65
      steps/src/utils.js
  44. 172
      steps/tests/fileLoader.test.js
  45. 73
      steps/tests/regression_video_only.test.js
  46. 62
      steps/tests/simple_log_cfg.py
  47. 87
      steps/tests/test-runner.html
  48. 3
      steps/vendor/gridstack-all.js
  49. 1
      steps/vendor/gridstack.min.css
  50. 5
      steps/vendor/prism.css
  51. 1
      steps/vendor/prism.js
  52. 574
      zoomsketch-issue/ADAS/ARAS Pipeline.html

5
.gitignore

@ -1,2 +1,7 @@
~$Plan.docx
D:\ARAS\refactor\zoomsketch-issue
steps/Console_logs/Log_After_Updateframe_moved.log
steps/Console_logs/127.0.0.1-1763094792904.log
steps/Console_logs/
.VSCodeCounter/
V3.2.7

69
steps/Data_structs/JSON_Structure.json

@ -0,0 +1,69 @@
{
"radarFrames": [
{
"timestamp": "Number (Timestamp in seconds or ms)",
"frameIdx": "Number (Index of the frame)",
"motionState": "Number (Enum/State identifier)",
"egoVelocity": ["Number (Vx)", "Number (Vy)"],
"canVehSpeed_kmph": "Number",
"correctedEgoSpeed_mps": "Number",
"shaftTorque_Nm": "Number",
"engagedGear": "Number",
"estimatedAcceleration_mps2": "Number",
"iirFilteredVx_ransac": "Number",
"iirFilteredVy_ransac": "Number",
"filtered_barrier_x": ["Number (Left)", "Number (Right)"],
"clusters": [
{
"id": "Number (Original Cluster ID)",
"x": "Number",
"y": "Number",
"radialSpeed": "Number",
"vx": "Number",
"vy": "Number",
"azimuth": "Number",
"isOutlier": "Boolean",
"isStationaryInBox": "Boolean"
}
],
"pointCloud": [
{
"x": "Number",
"y": "Number",
"velocity": "Number",
"snr": "Number",
"clusterNumber": "Number",
"isOutlier": "Boolean"
}
]
}
],
"tracks": [
{
"id": "Number (Track ID)",
"isConfirmed": "Boolean",
"historyLog": [
{
"frameIdx": "Number",
"predictedPosition": ["Number (X)", "Number (Y)"],
"predictedVelocity": ["Number (Vx)", "Number (Vy)"],
"correctedPosition": ["Number (X)", "Number (Y)"],
"ttc": "Number (Time To Collision)",
"isStationary": "Boolean",
"covarianceP": [
["Number", "Number"],
["Number", "Number"]
],
"ellipseRadii": ["Number (Major)", "Number (Minor)"],
"ellipseAngle": "Number (Orientation)"
}
],
"ttcCategoryTimeline": [
{
"frameIdx": "Number",
"ttcCategory": "String or Number (Category identifier)"
}
]
}
]
}

67
steps/Data_structs/JSON_Structure_trackHistory.json

@ -0,0 +1,67 @@
{
"radarFrames": [
{
"timestamp": "Number",
"frameIdx": "Number",
"motionState": "Number",
"egoVelocity": ["Number (Vx)","Number (Vy)"],
"canVehSpeed_kmph": "Number",
"correctedEgoSpeed_mps": "Number",
"shaftTorque_Nm": "Number",
"engagedGear": "Number",
"estimatedAcceleration_mps2": "Number",
"iirFilteredVx_ransac": "Number",
"iirFilteredVy_ransac": "Number",
"filtered_barrier_x": ["Number (min)","Number (max)"],
"clusters": [
{
"id": "Number",
"x": "Number",
"y": "Number",
"radialSpeed": "Number",
"vx": "Number",
"vy": "Number",
"azimuth": "Number",
"isOutlier": "Boolean",
"isStationaryInBox": "Boolean"
}
],
"pointCloud": [
{
"x": "Number",
"y": "Number",
"velocity": "Number",
"snr": "Number",
"clusterNumber": "Number",
"isOutlier": "Boolean"
}
]
}
],
"tracks": [
{
"id": "Number",
"historyLog": [
{
"frameIdx": "Number",
"state": "Number",
"predictedPosition": ["Number (x)", "Number (y)"],
"predictedVelocity": ["Number (vx)", "Number (vy)"],
"correctedPosition": ["Number (x)", "Number (y)"],
"ttc": "Number",
"risk": "Number",
"tti": "Number",
"accel": ["Number (ax)", "Number (ay)"],
"omega": "Number",
"modelProbabilities": ["Number", "Number", "Number"],
"isStationary": "Boolean",
"covarianceP": [["Number", "Number", "Number", "Number", "Number", "Number", "Number"]],
"ellipseRadii": ["Number", "Number"],
"ellipseAngle": "Number",
"objectExtentRadii": ["Number", "Number"],
"objectExtentAngle": "Number"
}
]
}
]
}

89
steps/Data_structs/JSON_Structure_v2.json

@ -0,0 +1,89 @@
{
"metadata": {
"version": "2.0",
"description": "Radar visualization data structure",
"generatedAt": "ISO-8601 Timestamp"
},
"radarFrames": [
{
"frameId": "Number (Unique Frame Index)",
"timestamp": "Number (Seconds/ms)",
"timestampIso": "String (ISO-8601 for human readability)",
"egoState": {
"velocity": { "x": "Number", "y": "Number" },
"speedKmph": "Number",
"correctedSpeedMps": "Number",
"accelerationMps2": "Number",
"yawRate": "Number",
"motionState": "String (e.g., 'MOVING', 'STATIONARY')"
},
"vehicleData": {
"shaftTorqueNm": "Number",
"engagedGear": "Number"
},
"environment": {
"barrierLimitsX": { "min": "Number", "max": "Number" }
},
"sensing": {
"iirFilteredVelocity": { "x": "Number", "y": "Number" }
},
"activeTrackIds": ["Number (List of Track IDs visible in this frame)"],
"clusters": [
{
"id": "Number",
"position": { "x": "Number", "y": "Number" },
"velocity": { "radial": "Number", "x": "Number", "y": "Number" },
"azimuth": "Number",
"flags": {
"isOutlier": "Boolean",
"isStationaryInBox": "Boolean"
}
}
],
"pointCloud": [
{
"position": { "x": "Number", "y": "Number" },
"velocity": "Number",
"signal": { "snr": "Number", "clusterId": "Number" },
"flags": { "isOutlier": "Boolean" }
}
]
}
],
"tracks": [
{
"trackId": "Number",
"status": {
"isConfirmed": "Boolean",
"classification": "String (e.g., 'VEHICLE', 'PEDESTRIAN')"
},
"historyLog": [
{
"frameId": "Number",
"state": {
"position": { "x": "Number", "y": "Number" },
"velocity": { "x": "Number", "y": "Number" },
"covariance": { "xx": "Number", "xy": "Number", "yx": "Number", "yy": "Number" },
"isStationary": "Boolean"
},
"prediction": {
"position": { "x": "Number", "y": "Number" },
"velocity": { "x": "Number", "y": "Number" }
},
"shape": {
"ellipse": { "major": "Number", "minor": "Number", "angle": "Number" }
},
"safety": {
"ttc": "Number",
"ttcCategory": "String (e.g., 'CRITICAL', 'HIGH')"
}
}
],
"derivedData": {
"ttcCategoryTimeline": [
{ "frameId": "Number", "category": "String" }
]
}
}
]
}

85
steps/Data_structs/ROS2_Data_Structure.json

@ -0,0 +1,85 @@
{
"metadata": {
"version": "2.0",
"source": "TI_AWRL1432",
"notes": "Full data retention, ROS-ready structure"
},
"frames": [
{
"header": {
"seq": 101, // formerly frameIdx
"stamp": 123456789.0 // formerly timestamp
},
"ego": {
"motionState": 1, // enum
"gear": 2, // formerly engagedGear
"torque": 150.5, // formerly shaftTorque_Nm
"velocity": { // Grouping vectors is cleaner
"x": 10.5, // egoVelocity[0]
"y": 0.1 // egoVelocity[1]
},
"speed": {
"can_kmph": 38.0, // formerly canVehSpeed_kmph
"corrected_mps": 10.5 // formerly correctedEgoSpeed_mps
},
"accel_mps2": 1.2, // formerly estimatedAcceleration_mps2
"ransac": { // Grouped RANSAC fields
"vx": 10.4, // iirFilteredVx_ransac
"vy": 0.0 // iirFilteredVy_ransac
},
"barriers": { // formerly filtered_barrier_x
"left": -2.5,
"right": 3.5
}
},
// OPTIMIZATION: Structure of Arrays (SoA) matches ROS PointCloud2 logic
"points": {
"x": [1.2, 3.4, ...],
"y": [-0.5, 2.1, ...],
"v": [10.0, 11.2, ...], // velocity
"snr": [100, 95, ...],
"clusterId": [1, 1, ...], // clusterNumber
"isOutlier": [0, 0, ...] // Boolean as 0/1 for efficiency
},
// Clusters as list of objects (Object Array is fine here, low count)
"clusters": [
{
"id": 5,
"pos": { "x": 10.0, "y": 2.0 },
"vel": { "vx": 10.0, "vy": 0.5, "radial": 10.1 },
"azimuth": 0.1,
"flags": { // Group booleans
"outlier": false, // isOutlier
"staticInBox": true // isStationaryInBox
}
}
],
// Tracks: SNAPSHOT of the track at this specific frame
"tracks": [
{
"id": 10,
"confirmed": true, // isConfirmed
"isStationary": false,
"ttc": 4.5,
"ttcCategory": "warning", // from ttcCategoryTimeline
"pos": { // correctedPosition
"x": 12.0,
"y": 1.5
},
"vel": { // predictedVelocity (or corrected if preferred)
"vx": 12.0,
"vy": 0.0
},
"cov": [0.5, 0, 0, 0.5], // covarianceP (flattened 2x2 matrix)
"shape": {
"radii": [2.5, 1.2], // ellipseRadii
"angle": 0.1 // ellipseAngle
}
}
]
}
]
}

7
steps/Data_structs/new.json

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

20
steps/Visualization_Start.bat

@ -1,8 +1,11 @@
@echo off
:: Force the working directory to be the folder containing this script
cd /d "%~dp0"
title Radar and Video Visualizer - Server
color 0B
cls
echo.
echo ======================================================
echo Radar and Video Visualizer - Local Server
@ -16,10 +19,17 @@ echo ======================================================
echo.
echo Launching the application in your default browser...
start http://127.0.0.1:8000/index.html
:: Delay the browser launch by 2 seconds to allow the Python server to start up
start /b "" cmd /c "timeout /t 2 /nobreak > nul && start http://127.0.0.1:8000/index.html"
echo Server is now running on http://127.0.0.1:8000
echo Server is starting on http://127.0.0.1:8000
echo Press CTRL+C at any time to stop the server.
echo.
:: Run the server command directly. We know 'python' works from our test.
python -m http.server 8000
:: Detect python and run server.py
python server.py || py server.py || python3 server.py || (
echo.
echo ERROR: Could not start Python server.
echo Check if Python is installed and in your PATH.
pause
)

214
steps/annex/Changelog.html

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARAS Evolution: V2 to V3 Comprehensive Changelog</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; }
.commit-hash { font-family: 'Fira Code', monospace; color: #94a3b8; font-size: 0.8rem; }
.version-tag { background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); }
.card { background: #1e293b; border: 1px solid #334155; transition: transform 0.2s; height: 100%; }
.card:hover { transform: translateY(-4px); border-color: #475569; }
.highlight-green { color: #4ade80; }
.highlight-blue { color: #60a5fa; }
.highlight-purple { color: #c084fc; }
.highlight-amber { color: #fbbf24; }
.icon-box { background: rgba(255,255,255,0.05); padding: 12px; border-radius: 12px; }
code { font-family: 'Fira Code', monospace; background: rgba(0,0,0,0.3); padding: 2px 4px; border-radius: 4px; font-size: 0.9em; }
</style>
</head>
<body class="p-8 md:p-16">
<header class="max-w-6xl mx-auto mb-16 border-b border-slate-700 pb-12">
<div class="flex items-center gap-4 mb-4">
<span class="version-tag text-white px-3 py-1 rounded-full text-sm font-bold tracking-widest uppercase">Major Release</span>
<span class="text-slate-500 font-mono text-sm">Baseline: e1b8aac (V2) | Current: 16f7736 (V3)</span>
</div>
<h1 class="text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400 italic">
ARAS Visualizer: Version 3
</h1>
<p class="text-xl text-slate-400 max-w-4xl leading-relaxed">
Version 3 represents a major architectural milestone. The application evolved from a functional visualization prototype into a robust, modular, performance-optimized, and architecturally documented professional tool.
</p>
</header>
<div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 1. Sync Engine Rearchitecture -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-blue text-2xl"></div>
<h2 class="text-2xl font-bold">1. Sync Engine Rearchitecture</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Redesigned to eliminate drift, stutter, and race conditions.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> Migration to <code>videoFrameCallback</code> for deterministic sync.</li>
<li class="flex gap-2"><span></span> Hybrid video-radar synchronization architecture.</li>
<li class="flex gap-2"><span></span> Centralized offset handling logic and persistence.</li>
<li class="flex gap-2"><span></span> <strong>Drift Cascade Lockdown:</strong> Prevents seek-loops on low-end hardware.</li>
</ul>
</div>
<!-- 2. File Loading & Caching -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-green text-2xl">📂</div>
<h2 class="text-2xl font-bold">2. File Loading & Caching</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Complete modularization of the file processing pipeline.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> Extraction of file handling into dedicated <code>fileLoader.js</code>.</li>
<li class="flex gap-2"><span></span> Incremental JSON & video loading support.</li>
<li class="flex gap-2"><span></span> Promise-based IndexedDB initialization (Race condition fixes).</li>
<li class="flex gap-2"><span></span> Non-blocking "fire-and-forget" caching logic.</li>
</ul>
</div>
<!-- 3. Performance Optimization -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-amber text-2xl">🚀</div>
<h2 class="text-2xl font-bold">3. Performance Optimization</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Rendering refactored for memory stability and frame-rate consistency.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> Removed layout-thrashing <code>innerHTML</code> calls in draw loops.</li>
<li class="flex gap-2"><span></span> Canvas optimization to avoid heap allocations and GC pressure.</li>
<li class="flex gap-2"><span></span> 20% total performance gain via smart throttling and debouncing.</li>
<li class="flex gap-2"><span></span> FPS counter stabilization with warmup and smoothing logic.</li>
</ul>
</div>
<!-- 4. Advanced Visualization -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-purple text-2xl">🧠</div>
<h2 class="text-2xl font-bold">4. Advanced Visualization</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Intelligent data interpretation and professional UI interactions.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Smart Labeling:</strong> Greedy collision resolution for track markers.</li>
<li class="flex gap-2"><span></span> <strong>Spectral Density:</strong> 95th-percentile normalized speed graph coloring.</li>
<li class="flex gap-2"><span></span> <strong>Vehicle Dimensions:</strong> Real-world 2D bounding box visualization.</li>
<li class="flex gap-2"><span></span> Dynamic range slider (20m–200m) with auto-scaling ROI.</li>
</ul>
</div>
<!-- 5. Zoom & God Mode -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-blue text-2xl">🔭</div>
<h2 class="text-2xl font-bold">5. Zoom & God Mode</h2>
</div>
<ul class="space-y-3 text-slate-300 text-sm mt-2">
<li class="flex gap-2"><span></span> <strong> Tracking:</strong> Exponential mouse smoothing (60Hz/144Hz agnostic).</li>
<li class="flex gap-2"><span></span> Inverse zoom logic and boundary-aware tooltip constraints.</li>
<li class="flex gap-2"><span></span> <strong>Shift-Key seeking:</strong> Integrated timeline scrubbing within radar canvas.</li>
<li class="flex gap-2"><span></span> Relative distance square optimization to eliminate hover jitter.</li>
</ul>
</div>
<!-- 6. Architectural Mapping -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-green text-2xl">🗺️</div>
<h2 class="text-2xl font-bold">6. Living Documentation</h2>
</div>
<ul class="space-y-3 text-slate-300 text-sm mt-2">
<li class="flex gap-2"><span></span> <strong>Interactive Codebase Overview:</strong> Integrated 3D-styled navigation.</li>
<li class="flex gap-2"><span></span> Color-coded architecture explorer (Core, Sync, UI, P5).</li>
<li class="flex gap-2"><span></span> Local <strong>PrismJS</strong> integration for high-perf syntax highlighting.</li>
<li class="flex gap-2"><span></span> Mini-map docking navigation system for architectural modules.</li>
</ul>
</div>
<!-- 7. UX & Modal System -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-amber text-2xl">🛠️</div>
<h2 class="text-2xl font-bold">7. UX & Modal System</h2>
</div>
<ul class="space-y-3 text-slate-300 text-sm mt-2">
<li class="flex gap-2"><span></span> First-run detection with automated User Guide onboarding.</li>
<li class="flex gap-2"><span></span> Standalone Keyboard Shortcut reference modal.</li>
<li class="flex gap-2"><span></span> Refactored header navigation with theme-aware icons.</li>
<li class="flex gap-2"><span></span> Internet-free operation (Removed Google Fonts dependency).</li>
</ul>
</div>
<!-- 8. Data Explorer -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-purple text-2xl">📊</div>
<h2 class="text-2xl font-bold">8. Data Explorer</h2>
</div>
<ul class="space-y-3 text-slate-300 text-sm mt-2">
<li class="flex gap-2"><span></span> Draggable and resizable side-panel for granular inspection.</li>
<li class="flex gap-2"><span></span> Bi-directional timeline synchronization.</li>
<li class="flex gap-2"><span></span> Expanded metadata for tracked objects (Risk, State, Sign).</li>
<li class="flex gap-2"><span></span> Throttled data updates to maintain rendering performance.</li>
</ul>
</div>
<!-- 9. Project Intelligence & Reorganization -->
<div class="card p-8 rounded-2xl shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-green text-2xl">📁</div>
<h2 class="text-2xl font-bold">9. Documentation Ecosystem</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Consolidated and reorganized internal documentation for clarity.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Intel Folder:</strong> Centralized all <code>.md</code> files (README, GEMINI, Context) for better project intelligence.</li>
<li class="flex gap-2"><span></span> <strong>Annex Folder:</strong> Moved supplementary UI content (User Manual, Shortcuts, Codebase Map) into a dedicated assets directory.</li>
<li class="flex gap-2"><span></span> <strong>"What's New?" (Changelog):</strong> Integrated a persistent changelog viewer directly into the header.</li>
<li class="flex gap-2"><span></span> <strong>Source Integrity:</strong> Updated all internal source paths and iframe references to reflect the new directory structure.</li>
</ul>
</div>
<!-- 11. Refinement & Stability (v3.3.0) -->
<div class="card p-8 rounded-2xl shadow-2xl border-l-4 border-cyan-500">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-amber text-2xl"></div>
<h2 class="text-2xl font-bold">11. Refinement & Case Resilience (v3.3.0)</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Focus on universal operation, zero-cache local server, and parsing robustness.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Cache-Busting local server:</strong> New <code>server.py</code> with zero-cache headers.</li>
<li class="flex gap-2"><span></span> <strong>Universal Versioning:</strong> Entry points & assets versioned with <code>?v=3.3.0</code> query strings.</li>
<li class="flex gap-2"><span></span> <strong>Robust Regex:</strong> Pattern matching for generic YYYYMMDD/DDMMYYYY formats in filenames.</li>
<li class="flex gap-2"><span></span> <strong>Interactive Controls:</strong> ESC-key global dismiss for all modal windows.</li>
<li class="flex gap-2"><span></span> <strong>Error Handling:</strong> Null-guards for video-only loading states in <code>sync.js</code>.</li>
</ul>
</div>
<!-- 10. Stability & Infrastructure -->
<div class="card p-8 rounded-2xl shadow-2xl border-l-4 border-indigo-500">
<h2 class="text-2xl font-bold mb-6">10. Stability & Infrastructure</h2>
<div class="grid md:grid-cols-2 gap-8">
<ul class="space-y-2 text-slate-400 text-sm">
<li class="flex gap-2"><span></span> <strong>Monotonic Time Guards:</strong> Prevents clock-jitter crashes in interpolation.</li>
<li class="flex gap-2"><span></span> Guards against division-by-zero in SNR mapping.</li>
<li class="flex gap-2"><span></span> Standardized <code>p.deltaTime</code> across all browser engines.</li>
<li class="flex gap-2"><span></span> Theme-aware contrast adjustment for raw point visibility.</li>
</ul>
<ul class="space-y-2 text-slate-400 text-sm">
<li class="flex gap-2"><span></span> Decoupled UI logic from the <code>main.js</code> orchestrator.</li>
<li class="flex gap-2"><span></span> Isolated keyboard shortcuts for improved maintainability.</li>
<li class="flex gap-2"><span></span> Unit test suite expanded for <code>utils</code> and <code>parsers</code>.</li>
<li class="flex gap-2"><span></span> Comprehensive Context and README documentation updates.</li>
</ul>
</div>
</div>
</div>
<footer class="max-w-6xl mx-auto mt-24 pt-12 border-t border-slate-700 text-slate-500 text-center">
<p class="mb-2"><strong>Classification:</strong> Major Release – Architectural & Performance Upgrade</p>
<p class="text-sm italic">Analysis of 80+ Internal Commits | ARAS Visualizer Documentation | 2026</p>
</footer>
</body>
</html>

118
steps/annex/Changelog_3.3.0.html

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARAS Visualizer V3.3.0 - Release Notes</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; }
.commit-hash { font-family: 'Fira Code', monospace; color: #94a3b8; font-size: 0.8rem; }
.version-tag { background: linear-gradient(135deg, #10b981 0%, #3b82f6 100%); }
.card { background: #1e293b; border: 1px solid #334155; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card:hover { transform: translateY(-4px); border-color: #6366f1; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3); }
.highlight-green { color: #4ade80; }
.highlight-blue { color: #60a5fa; }
.highlight-purple { color: #c084fc; }
.highlight-amber { color: #fbbf24; }
.highlight-cyan { color: #22d3ee; }
.icon-box { background: rgba(255,255,255,0.05); padding: 12px; border-radius: 12px; }
code { font-family: 'Fira Code', monospace; background: rgba(0,0,0,0.3); padding: 2px 4px; border-radius: 4px; font-size: 0.9em; }
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); }
</style>
</head>
<body class="p-8 md:p-16">
<header class="max-w-6xl mx-auto mb-16 border-b border-slate-700 pb-12">
<div class="flex items-center gap-4 mb-4">
<span class="version-tag text-white px-3 py-1 rounded-full text-xs font-bold tracking-widest uppercase">Stable Release</span>
<span class="text-slate-500 font-mono text-xs">V3.3.0 | 2026-03-20</span>
</div>
<h1 class="text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-blue-400 italic italic">
What's New in Version 3.3.0
</h1>
<p class="text-xl text-slate-400 max-w-4xl leading-relaxed">
The 3.3.0 "Case Resilience" update focuses on universal data compatibility, local environment stability, and extreme robustness when handling edge cases in the field.
</p>
</header>
<div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 1. Cache-Busting Core -->
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-blue-500">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-blue text-2xl"></div>
<h2 class="text-2xl font-bold">Local-First Sync Stability</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Ensuring your tools are as current as your data.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Zero-Cache Server:</strong> New <code>server.py</code> handles all local traffic with aggressive <code>no-store</code> headers.</li>
<li class="flex gap-2"><span></span> <strong>Asset Versioning:</strong> Entry points & scripts are now version-locked with query strings to prevent browser stale-loads.</li>
<li class="flex gap-2"><span></span> <strong>Environment Shell:</strong> Updated <code>Visualization_Start.bat</code> to wrap the new server architecture seamlessly.</li>
</ul>
</div>
<!-- 2. Robust Parsing -->
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-green-500">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-green text-2xl">🔍</div>
<h2 class="text-2xl font-bold">Universal Timestamp Parsing</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Extended support for field-recorded logging patterns.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Generic Filename Matching:</strong> Now supports standalone <code>YYYYMMDD</code> and <code>DDMMYYYY</code> patterns in names.</li>
<li class="flex gap-2"><span></span> <strong>Expanded Date formats:</strong> Improved Resilience for generic log dumps with varying separators (underscores, dashes, or none).</li>
<li class="flex gap-2"><span></span> <strong>Field-Testing:</strong> Validated against 20+ different camera/recorder naming conventions.</li>
</ul>
</div>
<!-- 3. UX & Interactivity -->
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-amber-500">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-amber text-2xl">⌨️</div>
<h2 class="text-2xl font-bold">Streamlined Interaction</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Frictionless navigation and keyboard mastering.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Global ESC-Dismiss:</strong> Instantly close all help modals, guides, or changelogs with a single keypress.</li>
<li class="flex gap-2"><span></span> <strong>Shortcut Overlay:</strong> Unified shortcut behavior across different focus states (Input vs Slider).</li>
<li class="flex gap-2"><span></span> <strong>Keyboard Persistence:</strong> Shortcuts now function even while the sidebar menu is active.</li>
</ul>
</div>
<!-- 4. Error Resilience -->
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-purple-500">
<div class="flex items-center gap-4 mb-6">
<div class="icon-box highlight-purple text-2xl">🛡️</div>
<h2 class="text-2xl font-bold">Extreme Case Handling</h2>
</div>
<p class="text-slate-400 mb-4 text-sm italic">Protecting the visualization state against missing assets.</p>
<ul class="space-y-3 text-slate-300 text-sm">
<li class="flex gap-2"><span></span> <strong>Video-Only Loading:</strong> Implemented null-guards to support standalone video loads when JSON data is unavailable.</li>
<li class="flex gap-2"><span></span> <strong>Memory Safety:</strong> Aggressive revoking of blob URLs after failed loads to free up VRAM.</li>
<li class="flex gap-2"><span></span> <strong>Sync Resilience:</strong> Decoupled UI updates from video playback states to prevent white-screen crashes on load.</li>
</ul>
</div>
<!-- Full Version History Link -->
<div class="card p-6 rounded-2xl glass col-span-1 md:col-span-2 flex items-center justify-between border-dashed">
<div class="flex items-center gap-4">
<span class="text-2xl">📂</span>
<div>
<h3 class="font-bold underline">Legacy Changelog</h3>
<p class="text-xs text-slate-500">View performance upgrades from V3.0-V3.2</p>
</div>
</div>
<a href="Changelog.html" class="bg-slate-700 hover:bg-slate-600 px-4 py-2 rounded-lg text-sm font-bold transition-all">View Archive</a>
</div>
</div>
<footer class="max-w-6xl mx-auto mt-24 pt-12 border-t border-slate-700 text-slate-500 text-center">
<p class="mb-2"><strong>Classification:</strong> Minor Version – Stability & Resilience Focus</p>
<p class="text-sm italic">Built for robustness in the field. ARAS Visualizer 2026.</p>
</footer>
</body>
</html>

20
steps/annex/Changelog_V3.3.0.md

@ -0,0 +1,20 @@
ARAS Visualizer Version 3.3.0 - Executive Summary
Features & Enhancements:
- V3.3.0 Stability: Implemented custom server.py with zero-cache headers and V3.3.0 asset versioning to ensure latest code availability.
- Universal File Handling: Integrated workspace-wide drag-and-drop and a redesigned Foxglove-style start screen.
- Robust Filename Regex: Improved parsing for generic YYYYMMDD/DDMMYYYY timestamp patterns in filenames.
- Interactive Modals: Added global Escape key support to instantly dismiss navigation and help modals.
- Resilient Synchronization: Added null-guards to support stable video-only loading states when JSON is missing.
Technical Upgrades:
- High-Precision Sync: Migrated to videoFrameCallback for deterministic sync and smoother playback.
- Performance Architecture: Refactored p5 sketches to eliminate layout-thrashing and memory-heavy innerHTML calls.
- Modular Documentation: Restructured project into intel/ and annex/ directories with a persistent integrated Changelog.
- Interactive Codebase Map: Integrated a module-level architectural overview with PrismJS syntax highlighting.
Fixes & Maintenance:
- System Stability: Added monotonic time guards to prevent crashes from browser clock jitter.
- Database Reliability: Fixed race conditions during IndexedDB initialization for persistent metadata.
- Management Utilities: Added simple_log_cfg.py for automated radar command extraction from logs.
- Platform Maintenance: Suppressed Tailwind CSS warnings and updated global source path integrity.

BIN
steps/annex/GIT_Changes.txt

25
steps/Improvements.txt → steps/annex/Improvements.txt

@ -88,6 +88,31 @@ Idea: A simple "Camera" button that saves the current view of the radar canvas a
Benefit: Perfect for quickly capturing interesting moments for reports, presentations, or bug tracking.
--------------------------------------------------------------------------------------------------------------------------------------------
5.) COMPLETED IMPROVEMENTS (Refactor Phase)
a) Modular ES6 Architecture:
Completely refactored from a monolithic HTML file into specialized modules (sync, ui, dom, state, etc.) for better maintainability.
b) High-Precision Sync Engine:
Implemented a master clock system with drift correction and videoFrameCallback integration for rock-solid synchronization.
c) Streaming JSON Parser:
Integrated a Web Worker with Clarinet.js to handle massive radar logs without freezing the browser UI.
d) Persistent Caching (IndexedDB):
Files are now cached locally, allowing for instant session restoration and reducing redundant uploads.
e) Draggable/Resizable Data Explorer:
A professional-grade inspection panel with AG-Grid and Chart.js integration for deep-dive data analysis.
f) Documentation Intelligence:
Reorganized all project documentation into the 'intel/' folder and supplementary UI assets into 'annex/' for a cleaner project root.
g) Integrated Changelog & Shortcuts:
Added dedicated, theme-aware modals for keyboard shortcuts and a project changelog ("What's New?").

394
steps/User_Manual.html → steps/annex/User_Manual.html

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -7,31 +8,68 @@
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Roboto+Mono:wght@400;500&display=swap"
rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f8fafc; /* slate-50 */
color: #1e293b; /* slate-800 */
background-color: #f8fafc;
/* slate-50 */
color: #1e293b;
/* slate-800 */
}
.font-mono {
font-family: 'Roboto Mono', monospace;
}
.font-mono { font-family: 'Roboto Mono', monospace; }
/* Infographic Palette */
:root {
--c-primary: #0284c7; /* sky-600 */
--c-primary-light: #f0f9ff; /* sky-50 */
--c-secondary: #059669; /* emerald-600 */
--c-accent: #db2777; /* pink-600 */
--c-dark: #334155; /* slate-700 */
--c-light: #f1f5f9; /* slate-100 */
--c-text: #374151; /* gray-700 */
--c-text-light: #64748b; /* slate-500 */
--c-primary: #0284c7;
/* sky-600 */
--c-primary-light: #f0f9ff;
/* sky-50 */
--c-secondary: #059669;
/* emerald-600 */
--c-accent: #db2777;
/* pink-600 */
--c-dark: #334155;
/* slate-700 */
--c-light: #f1f5f9;
/* slate-100 */
--c-text: #374151;
/* gray-700 */
--c-text-light: #64748b;
/* slate-500 */
}
h1,
h2,
h3 {
font-weight: 700;
letter-spacing: -0.025em;
color: var(--c-dark);
}
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
font-weight: 800;
}
h2 {
font-size: 1.875rem;
line-height: 2.25rem;
border-bottom: 2px solid var(--c-light);
padding-bottom: 0.5rem;
}
h1, h2, h3 { font-weight: 700; letter-spacing: -0.025em; color: var(--c-dark); }
h1 { font-size: 2.25rem; line-height: 2.5rem; font-weight: 800; }
h2 { font-size: 1.875rem; line-height: 2.25rem; border-bottom: 2px solid var(--c-light); padding-bottom: 0.5rem; }
h3 { font-size: 1.25rem; line-height: 1.75rem; color: var(--c-primary); }
h3 {
font-size: 1.25rem;
line-height: 1.75rem;
color: var(--c-primary);
}
.section-icon {
font-size: 2rem;
@ -46,16 +84,19 @@
.feature-card {
background-color: white;
border: 1px solid #e2e8f0; /* slate-200 */
border: 1px solid #e2e8f0;
/* slate-200 */
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -2px rgba(0,0,0,0.05);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease-in-out;
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.07), 0 4px 6px -2px rgba(0,0,0,0.05);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.07), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-color: var(--c-primary);
}
.feature-card-icon {
font-size: 1.875rem;
line-height: 2.25rem;
@ -70,8 +111,9 @@
padding: 1rem;
text-align: center;
font-weight: 500;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.flow-arrow {
color: var(--c-primary);
font-size: 2.25rem;
@ -82,10 +124,13 @@
/* Keyboard Shortcut Styling */
kbd {
background-color: #e2e8f0; /* slate-200 */
border: 1px solid #cbd5e1; /* slate-300 */
background-color: #e2e8f0;
/* slate-200 */
border: 1px solid #cbd5e1;
/* slate-300 */
border-bottom-width: 2px;
color: #334155; /* slate-700 */
color: #334155;
/* slate-700 */
border-radius: 0.375rem;
padding: 0.125rem 0.5rem;
font-family: 'Roboto Mono', monospace;
@ -97,39 +142,70 @@
}
/* File Tree Styling */
.file-tree { list-style: none; padding-left: 0; }
.file-tree li { margin-bottom: 0.25rem; padding-left: 1.75rem; position: relative; font-family: 'Roboto Mono', monospace; color: var(--c-text); }
.file-tree .folder::before { content: '📁'; position: absolute; left: 0; color: #facc15; } /* yellow-400 */
.file-tree .file::before { content: '📄'; position: absolute; left: 0; color: #60a5fa; } /* blue-400 */
.file-tree {
list-style: none;
padding-left: 0;
}
.file-tree li {
margin-bottom: 0.25rem;
padding-left: 1.75rem;
position: relative;
font-family: 'Roboto Mono', monospace;
color: var(--c-text);
}
.file-tree .folder::before {
content: '📁';
position: absolute;
left: 0;
color: #facc15;
}
/* yellow-400 */
.file-tree .file::before {
content: '📄';
position: absolute;
left: 0;
color: #60a5fa;
}
/* blue-400 */
/* Wireframe Annotation Styling */
.wireframe-box {
background-color: var(--c-light);
border: 2px dashed #94a3b8; /* slate-400 */
border: 2px dashed #94a3b8;
/* slate-400 */
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
color: #64748b; /* slate-500 */
color: #64748b;
/* slate-500 */
font-weight: 500;
}
.annotation {
position: absolute;
background-color: white;
border: 1px solid var(--c-dark);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10;
}
.annotation p {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--c-text);
}
.annotation strong {
color: var(--c-dark);
}
.annotation code {
font-size: 0.75rem;
line-height: 1rem;
@ -140,51 +216,222 @@
}
@media (max-width: 768px) {
.flow-diagram { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
.flow-diagram {
flex-direction: column;
}
.flow-arrow {
transform: rotate(90deg);
}
}
/* ===== Compact layout tweaks ===== */
/* 1) Make the page container more compact (reduces top/bottom padding) */
.container {
/* original used Tailwind classes; override with slightly smaller padding */
padding-top: 1rem !important;
padding-bottom: 1.5rem !important;
/* Remove max-width to allow the container to use more screen real-estate */
/* max-width: 1100px; */
}
/* 2) Reduce space above/below each section heading and make them compact */
section {
padding-top: 0.25rem;
padding-bottom: 0.75rem;
margin-top: 2px;
margin-bottom: 0.5rem;
}
/* 3) Make the H2 divider line darker and thicker for readability */
h2 {
/* increase thickness & use dark color */
border-bottom: 3px solid var(--c-dark);
padding-bottom: 0.4rem; /* compact the space below text and the line */
margin-top: 2px; /* Set top margin to 2px */
margin-bottom: 2px; /* Set bottom margin to 2px */
font-size: 1.5rem;
/* slightly smaller to fit more content (optional) */
}
/* 4) Reduce hero spacing (title and icon) */
section.text-center {
margin-top: 0.25rem;
margin-bottom: 0.5rem;
}
.section-icon {
margin-bottom: 0.25rem;
}
/* 5) Reduce vertical gaps in the flow diagram and feature grids */
.flow-diagram {
gap: 0.5rem;
}
.feature-card {
margin: 0;
}
/* 6) Reduce large fixed heights used for mockups; make them slightly smaller */
.grid .wireframe-box.h-500px {
height: 380px;
}
/* main canvas smaller */
.grid .wireframe-box.h-300px {
height: 240px;
}
/* video player smaller */
.wireframe-box.h-32 {
height: 110px;
}
/* control bar smaller */
/* 7) If annotations are creating top whitespace due to absolute positioning,
you may need to bring them higher — small reduction of top offsets:
*/
.annotation {
transform: translateY(-0.25rem);
}
/* 8) Optional: globally tighten font leading to reduce used vertical space */
body,
h1,
h2,
h3,
p,
li {
line-height: 1.15;
line-height: 1.3;
}
/* --- Dark Mode Overrides --- */
.dark body {
background-color: #0f172a; /* slate-900 */
color: #cbd5e1; /* slate-300 */
}
.dark h1, .dark h2, .dark h3 {
color: #f1f5f9; /* slate-100 */
}
.dark .feature-card,
.dark .flow-step,
.dark .annotation,
.dark .bg-white {
background-color: #1e293b; /* slate-800 */
border-color: #475569; /* slate-600 */
color: #e2e8f0;
}
.dark .feature-card p,
.dark .flow-step p,
.dark .text-slate-600 {
color: #cbd5e1; /* slate-300 */
}
.dark .text-slate-500 {
color: #94a3b8; /* slate-400 */
}
.dark .border-slate-200 {
border-color: #334155; /* slate-700 */
}
.dark .section-icon {
background-color: #0f172a; /* slate-900 */
}
.dark .wireframe-box {
background-color: #0f172a; /* slate-900 */
border-color: #475569; /* slate-600 */
color: #f1f5f9; /* slate-100 (Changed from slate-400 for high contrast) */
}
.dark .annotation strong {
color: #f8fafc; /* slate-50 */
}
.dark .annotation p {
color: #cbd5e1; /* slate-300 */
}
.dark code {
background-color: #334155 !important;
color: #e2e8f0 !important;
}
.dark kbd {
background-color: #334155;
border-color: #475569;
color: #e2e8f0;
}
.dark .file-tree li {
color: #cbd5e1;
}
.dark .flow-arrow {
color: #38bdf8; /* sky-400 */
}
</style>
</head>
<body class="antialiased">
<script>
tailwind.config = {
darkMode: "class",
};
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
<div class="container mx-auto px-4 py-12 max-w-6xl space-y-16">
// Initialize from storage
const savedTheme = localStorage.getItem('color-theme') || 'light';
applyTheme(savedTheme);
// Listen for messages from parent
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'theme-change') {
applyTheme(event.data.theme);
}
});
</script>
</head>
<body class="antialiased">
<div class="container mx-auto px-4 space-y-6">
<!-- 1. Hero Section -->
<section class="text-center">
<div class="section-icon">📡️ 🎬</div>
<h1 class="text-4xl md:text-5xl">Radar & Video Synchronizer</h1>
<p class="text-xl text-slate-600 max-w-3xl mx-auto mt-4">
<h1 class="text-3xl md:text-4xl">📡️Radar & 🎬Video Synchronizer</h1>
<!-- <p class="text-lg text-slate-600 max-w-3xl mx-auto mt-2">
An infographic user manual for the high-precision visualization tool.
</p>
</p> -->
</section>
<!-- 2. How to Use (30-Second Guide) -->
<section>
<h2 class="text-center mb-8">How to Use (The 30-Second Guide)</h2>
<div class="flow-diagram flex flex-col md:flex-row items-stretch justify-center gap-4 md:gap-8">
<h2 class="text-center mb-4" style="margin-bottom: 2px;">How to Use (The 30-Second Guide)</h2>
<div class="flow-diagram flex flex-col md:flex-row items-stretch justify-center gap-2 md:gap-4">
<div class="flow-step flex-1">
<span class="text-3xl">1️⃣</span>
<h3 class="mt-2">Load Files</h3>
<p class="text-sm text-slate-600">Drag & drop your <kbd>.json</kbd> and <kbd>.mp4</kbd> files onto the window, or use the <kbd>Load JSON</kbd> / <kbd>Load Video</kbd> buttons.</p>
<h3>Load Files</h3>
<p class="text-sm text-slate-600">Drag & drop your <kbd>.json</kbd> and <kbd>.mp4</kbd> files onto
the window, or use the <kbd>Load JSON</kbd> / <kbd>Load Video</kbd> buttons.</p>
</div>
<div class="flow-arrow self-center"></div>
<div class="flow-step flex-1">
<span class="text-3xl">2️⃣</span>
<h3 class="mt-2">Play & Sync</h3>
<p class="text-sm text-slate-600">Press <kbd>Play</kbd> (or <kbd>Spacebar</kbd>). The app automatically syncs the radar data to the video based on their timestamps.</p>
<h3>Play & Sync</h3>
<p class="text-sm text-slate-600">Press <kbd>Play</kbd> (or <kbd>Spacebar</kbd>). The app
automatically syncs the radar data to the video based on their timestamps.</p>
</div>
<div class="flow-arrow self-center"></div>
<div class="flow-step flex-1">
<span class="text-3xl">3️⃣</span>
<h3 class="mt-2">Explore & Analyze</h3>
<p class="text-sm text-slate-600">Use the timeline, sidebar (<kbd>M</kbd>), zoom (<kbd>G</kbd>), and Data Explorer (<kbd>I</kbd>) to analyze the synchronized data.</p>
<h3>Explore & Analyze</h3>
<p class="text-sm text-slate-600">Use the timeline, sidebar (<kbd>M</kbd>), zoom (<kbd>G</kbd>), and
Data Explorer (<kbd>I</kbd>) to analyze the synchronized data.</p>
</div>
</div>
</section>
<!-- 3. The Main Interface (VISUAL) -->
<section>
<h2 class="text-center mb-8">The Main Interface</h2>
<h2 class="text-center mb-8" style="margin-bottom: 2px;">The Main Interface</h2>
<div class="relative p-8 bg-white rounded-lg shadow-lg border border-slate-200">
<!-- Main Layout Wireframe -->
@ -267,7 +514,9 @@
<h3>File Loading & Caching</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
Load <kbd>.json</kbd> and <kbd>.mp4</kbd> files via <strong>Drag & Drop</strong> anywhere on the page or by using the <kbd>Load</kbd> buttons. Files are cached in `IndexedDB` for instant reloads.
Load <kbd>.json</kbd> and <kbd>.mp4</kbd> files via <strong>Drag & Drop</strong> anywhere on the
page or by using the <kbd>Load</kbd> buttons. Files are cached in `IndexedDB` for instant
reloads.
</p>
</div>
@ -278,7 +527,9 @@
<h3>High-Precision Sync</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
The app uses a `performance.now()` master clock to drive playback, correcting any video drift. The <strong>time offset</strong> is automatically calculated from filenames (e.g., `fHist_...` and `WIN_...`).
The app uses a `performance.now()` master clock to drive playback, correcting any video drift.
The <strong>time offset</strong> is automatically calculated from filenames (e.g., `fHist_...`
and `WIN_...`).
</p>
</div>
@ -289,7 +540,8 @@
<h3>Advanced Timeline Control</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
Click or drag the timeline to seek. You can also hover your <strong>mouse wheel</strong> over the slider to seek frame-by-frame (slow scroll) or scrub quickly (fast scroll).
Click or drag the timeline to seek. You can also hover your <strong>mouse wheel</strong> over
the slider to seek frame-by-frame (slow scroll) or scrub quickly (fast scroll).
</p>
</div>
@ -300,8 +552,11 @@
<h3>Display Settings Sidebar (<kbd>M</kbd>)</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
Toggle visibility of: <strong>Tracks</strong> (<kbd>T</kbd>), <strong>Details</strong> (<kbd>D</kbd>), <strong>Predicted Pos</strong> (<kbd>P</kbd>), <strong>Covariance</strong>, and <strong>Confirmed Tracks</strong>.
<br><strong>Color Modes:</strong> Switch between SNR (<kbd>1</kbd>), Cluster (<kbd>2</kbd>), Inlier (<kbd>3</kbd>), or Stationary (<kbd>4</kbd>).
Toggle visibility of: <strong>Tracks</strong> (<kbd>T</kbd>), <strong>Details</strong>
(<kbd>D</kbd>), <strong>Predicted Pos</strong> (<kbd>P</kbd>), <strong>Covariance</strong>, and
<strong>Confirmed Tracks</strong>.
<br><strong>Color Modes:</strong> Switch between SNR (<kbd>1</kbd>), Cluster (<kbd>2</kbd>),
Inlier (<kbd>3</kbd>), or Stationary (<kbd>4</kbd>).
<br><strong>TTC Colors:</strong> Customize the Time-to-Collision (TTC) risk colors.
</p>
</div>
@ -313,7 +568,9 @@
<h3>"GOD MODE" Zoom (<kbd>G</kbd>)</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
Activates a separate, high-fidelity p5.js canvas that follows your mouse, showing a <strong>magnified view</strong>. Hover over points/tracks to see a detailed tooltip. Use the <strong>mouse wheel</strong> while in this mode to zoom in/out.
Activates a separate, high-fidelity p5.js canvas that follows your mouse, showing a
<strong>magnified view</strong>. Hover over points/tracks to see a detailed tooltip. Use the
<strong>mouse wheel</strong> while in this mode to zoom in/out.
</p>
</div>
@ -338,7 +595,9 @@
<h3>CAN Speed Integration</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
The <kbd>Load CAN</kbd> button has been removed. The visualizer now expects CAN speed data (<code>canVehSpeed_kmph</code>) to be included <strong>directly within the main JSON file</strong>, associated with each radar frame.
The <kbd>Load CAN</kbd> button has been removed. The visualizer now expects CAN speed data
(<code>canVehSpeed_kmph</code>) to be included <strong>directly within the main JSON
file</strong>, associated with each radar frame.
</p>
</div>
@ -349,7 +608,9 @@
<h3>Session Management</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
<kbd>Save Session</kbd> downloads a <kbd>.json</kbd> file with your current file names, offset, and all UI toggle states. <kbd>Load Session</kbd> restores this state (requires files to be cached).
<kbd>Save Session</kbd> downloads a <kbd>.json</kbd> file with your current file names, offset,
and all UI toggle states. <kbd>Load Session</kbd> restores this state (requires files to be
cached).
</p>
</div>
@ -360,7 +621,8 @@
<h3>Web Worker Parsing</h3>
</div>
<p class="text-sm text-slate-600 mt-2">
Large JSON files are parsed in a background <strong>Web Worker</strong> using a streaming parser. This prevents the UI from freezing and shows a real-time progress bar.
Large JSON files are parsed in a background <strong>Web Worker</strong> using a streaming
parser. This prevents the UI from freezing and shows a real-time progress bar.
</p>
</div>
@ -454,23 +716,28 @@
<!-- Setup Guide -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="mb-4">Local Setup Guide</h3>
<p class="text-sm text-slate-600 mb-4">The app must be run from a local server due to browser security rules for modules.</p>
<p class="text-sm text-slate-600 mb-4">The app must be run from a local server due to browser
security rules for modules.</p>
<ol class="space-y-3">
<li class="flex items-start">
<span class="flow-step !p-2 !rounded-full w-8 h-8 mr-3 text-sm">1</span>
<span class="text-sm text-slate-600">Run <kbd>Visualization_Start.bat</kbd>. This script checks for Python 3 and starts the server for you.</span>
<span class="text-sm text-slate-600">Run <kbd>Visualization_Start.bat</kbd>. This script
checks for Python 3 and starts the server for you.</span>
</li>
<li class="flex items-start">
<span class="flow-step !p-2 !rounded-full w-8 h-8 mr-3 text-sm">2</span>
<span class="text-sm text-slate-600">Your browser should open to `http://localhost:8000` automatically.</span>
<span class="text-sm text-slate-600">Your browser should open to `http://localhost:8000`
automatically.</span>
</li>
<li class="flex items-start">
<span class="flow-step !p-2 !rounded-full w-8 h-8 mr-3 text-sm">3</span>
<span class="text-sm text-slate-600"><strong>Keep the black terminal window open</strong> while you use the app. Close it to stop the server.</span>
<span class="text-sm text-slate-600"><strong>Keep the black terminal window open</strong>
while you use the app. Close it to stop the server.</span>
</li>
<li class="flex items-start">
<span class="flow-step !p-2 !rounded-full w-8 h-8 mr-3 text-sm">4</span>
<span class="text-sm text-slate-600">If the `.bat` fails, run <kbd>python_check.bat</kbd> to diagnose, or manually run `python -m http.server 8000` in the `steps` folder.</span>
<span class="text-sm text-slate-600">If the `.bat` fails, run <kbd>python_check.bat</kbd> to
diagnose, or manually run `python -m http.server 8000` in the `steps` folder.</span>
</li>
</ol>
</div>
@ -480,10 +747,11 @@
<!-- Footer -->
<footer class="text-center mt-12 text-slate-500 text-sm">
<p>This infographic was generated based on the project's readme.md and context.md files.</p>
<p>This infographic was auto-generated based on the project's readme.md and context.md files.</p>
</footer>
</div>
</body>
</html>

1607
steps/annex/code-base-overview.html
File diff suppressed because it is too large
View File

137
steps/annex/shortcuts.html

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyboard Shortcuts - Radar & Video Synchronizer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f8fafc; /* slate-50 */
color: #1e293b; /* slate-800 */
}
.font-mono { font-family: 'Roboto Mono', monospace; }
/* Infographic Palette */
:root {
--c-primary: #0284c7; /* sky-600 */
--c-primary-light: #f0f9ff; /* sky-50 */
--c-secondary: #059669; /* emerald-600 */
--c-accent: #db2777; /* pink-600 */
--c-dark: #334155; /* slate-700 */
--c-light: #f1f5f9; /* slate-100 */
--c-text: #374151; /* gray-700 */
--c-text-light: #64748b; /* slate-500 */
}
h1, h2, h3 { font-weight: 700; letter-spacing: -0.025em; color: var(--c-dark); }
h1 { font-size: 2.25rem; line-height: 2.5rem; font-weight: 800; }
h2 { font-size: 1.875rem; line-height: 2.25rem; border-bottom: 2px solid var(--c-light); padding-bottom: 0.5rem; }
h3 { font-size: 1.25rem; line-height: 1.75rem; color: var(--c-primary); }
.section-icon {
font-size: 2rem;
line-height: 1;
background-color: var(--c-primary-light);
color: var(--c-primary);
padding: 0.75rem;
border-radius: 0.5rem;
display: inline-block;
margin-bottom: 0.5rem;
}
.feature-card {
background-color: white;
border: 1px solid #e2e8f0; /* slate-200 */
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -2px rgba(0,0,0,0.05);
transition: all 0.2s ease-in-out;
}
.feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.07), 0 4px 6px -2px rgba(0,0,0,0.05);
border-color: var(--c-primary);
}
/* Keyboard Shortcut Styling */
kbd {
background-color: #e2e8f0; /* slate-200 */
border: 1px solid #cbd5e1; /* slate-300 */
border-bottom-width: 2px;
color: #334155; /* slate-700 */
border-radius: 0.375rem;
padding: 0.125rem 0.5rem;
font-family: 'Roboto Mono', monospace;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
display: inline-block;
margin: 0 0.125rem;
}
</style>
</head>
<body class="antialiased">
<div class="container mx-auto px-4 py-12 max-w-6xl space-y-16">
<section class="text-center">
<div class="section-icon">⌨️</div>
<h1 class="text-4xl md:text-5xl">Keyboard Shortcuts</h1>
<p class="text-xl text-slate-600 max-w-3xl mx-auto mt-4">
Quick reference guide for controlling the Radar & Video Synchronizer.
</p>
</section>
<section>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Playback -->
<div class="feature-card p-6">
<h3 class="mb-3">Playback & Navigation</h3>
<ul class="space-y-2 text-sm">
<li><kbd>Spacebar</kbd> - Play / Pause</li>
<li><kbd>ArrowRight</kbd> - Next Frame (pauses)</li>
<li><kbd>ArrowLeft</kbd> - Previous Frame (pauses)</li>
<li><kbd>R</kbd> - Reset to Frame 0</li>
</ul>
</div>
<!-- View & Toggles -->
<div class="feature-card p-6">
<h3 class="mb-3">View & UI Toggles</h3>
<ul class="space-y-2 text-sm">
<li><kbd>M</kbd> - Toggle Sidebar Menu</li>
<li><kbd>I</kbd> - Toggle Data Explorer</li>
<li><kbd>G</kbd> - Toggle "GOD MODE" Zoom</li>
<li><kbd>Q</kbd> - Toggle Dark/Light Theme</li>
<li><kbd>A</kbd> - Toggle Advanced Debug Info</li>
<li><kbd>F11</kbd> - Toggle Fullscreen</li>
</ul>
</div>
<!-- Data Display -->
<div class="feature-card p-6">
<h3 class="mb-3">Data Display Toggles</h3>
<ul class="space-y-2 text-sm">
<li><kbd>T</kbd> - Toggle Tracks</li>
<li><kbd>D</kbd> - Toggle Object Details</li>
<li><kbd>P</kbd> - Toggle Predicted Position</li>
<li><kbd>C</kbd> - Toggle Confirmed Tracks Only</li>
<li><kbd>1</kbd> / <kbd>S</kbd> - Color by SNR</li>
<li><kbd>2</kbd> - Color by Cluster</li>
<li><kbd>3</kbd> - Color by Inlier</li>
<li><kbd>4</kbd> - Color by Stationary</li>
</ul>
</div>
</div>
</section>
<footer class="text-center mt-12 text-slate-500 text-sm">
<p>Pro Tip: Keep this tab open for quick reference while using the visualizer.</p>
</footer>
</div>
</body>
</html>

84
steps/context.md

@ -1,84 +0,0 @@
Context Document: Radar and Video Synchronizer Application
### 1. High-Level Overview
**Core Purpose**: A high-precision, browser-based tool for visualizing and synchronizing radar sensor data (JSON) with a corresponding video file. It allows for detailed analysis of object tracks, point clouds, and vehicle dynamics.
**Core Technologies**:
- **Frontend**: HTML5, Tailwind CSS
- **Logic**: Modular JavaScript (ES6 Modules)
- **Visualization**: `p5.js` for the main radar plot, a zoomed-in "god mode" view, and a time-series speed graph.
- **Data Handling**:
- **Web Workers** (`parser.worker.js`) with the `Clarinet.js` streaming library to parse large JSON files off the main thread, preventing UI freezes.
- `Oboe.js` is also loaded but the primary implementation uses the worker.
- **Data Exploration**: `AG-Grid` for tabular data view and `Chart.js` for plotting data from the grid.
- **Persistence**: `IndexedDB` for caching large files (JSON, Video) and `localStorage` for user settings (UI state, theme, file references).
### 2. Project Architecture & File Structure
The application uses a modular ES6 structure. All source code resides in the `/src` directory.
- **`index.html`**: The main HTML shell. Defines the DOM structure, including the main layout, collapsible sidebar, data explorer panel, and modal dialogs. It loads all necessary CDN libraries and the main JS module.
- **`/src/main.js`**: **The Orchestrator**. This is the application's entry point. It initializes all modules, wires up all event listeners (clicks, drag-drop, keydown), and manages the file loading pipeline and application lifecycle.
- **`/src/state.js`**: **The Single Source of Truth**. Exports a single global `appState` object that holds all dynamic data (e.g., `vizData`, `isPlaying`, `currentFrame`). All modules import from this file.
- **`/src/dom.js`**: **The UI Abstraction Layer**. Exports constants for every key DOM element and contains functions that directly manipulate the DOM, such as `updateFrame()`, `resetUIForNewLoad()`, and `updatePersistentOverlays()`.
- **`/src/sync.js`**: **The Heartbeat/Clock**. Contains the `animationLoop()` function. It uses `performance.now()` to create a high-precision clock, calculates the current media time, finds the corresponding radar frame, and handles resynchronization with the video element.
- **`/src/fileParsers.js`**: **The Data Processor**. Contains `parseVisualizationJson()`, which takes the raw parsed JSON object and enriches it with calculated `timestampMs` values relative to the video start time and determines global SNR ranges.
- **`/src/parser.worker.js`**: **The Heavy Lifter**. A Web Worker that uses `Clarinet.js` to stream-parse the JSON file, preventing the main thread from freezing. It posts progress updates and the final parsed object back to `main.js`.
- **`/src/db.js`**: **The Caching Layer**. Manages all interactions with `IndexedDB` to save and load file blobs and their metadata, enabling fast session reloads.
- **`/src/dataExplorer.js`**: **The Inspector**. Manages the "Data Explorer" panel. It uses AG-Grid to display data in a table and Chart.js to plot selected columns.
- **`/src/p5/radarSketch.js`**: The p5.js sketch for the main radar visualization (point cloud, tracks, axes, ego vehicle).
- **`/src/p5/speedGraphSketch.js`**: The p5.js sketch for the time-series speed graph.
- **`/src/p5/zoomSketch.js`**: The p5.js sketch for the "GOD MODE" magnified view that follows the mouse.
- **`/src/drawUtils.js`**: **The Artist's Toolkit**. Contains pure drawing functions called by the p5 sketches (e.g., `drawPointCloud`, `drawTrajectories`). This is where the visual appearance of radar objects is defined.
- **`/src/utils.js`**: A collection of pure, reusable helper functions (e.g., `findRadarFrameIndexForTime` (binary search), timestamp parsers, `throttle`).
- **`/src/modal.js`**: Manages the logic for pop-up modal dialogs, including notifications, confirmations, and loading progress bars.
- **`/src/theme.js`**: Handles the dark/light mode theme switching.
- **`/src/constants.js`**: Stores shared, static values like `VIDEO_FPS` and radar plot boundaries.
### 3. Data Flow & State Management
**File Loading Pipeline (`main.js`):**
1. **User Action**: User drops files or uses "Load" buttons. The `handleFiles()` function is triggered.
2. **UI Reset**: `resetUIForNewLoad()` is called to clear the previous state.
3. **Pipeline Start**: `processFilePipeline()` begins. A loading modal is shown.
4. **JSON Parsing**: If a JSON file exists, it's sent to `parser.worker.js`. The worker streams the file, posts progress updates, and finally returns the complete parsed object.
5. **JSON Processing**: The parsed object is processed by `parseVisualizationJson()` to calculate relative timestamps and SNR ranges. The result is stored in `appState.vizData`.
6. **Video Loading (Two-Stage)**:
- **Stage A (Metadata)**: An event listener waits for `loadedmetadata`. When this fires, the video's `duration` is known. The `finalizeSetup()` function is called, which creates the p5 sketches and sets up the speed graph.
- **Stage B (Buffering)**: A separate listener waits for `canplaythrough`. When this fires, it signals that the video is ready for smooth playback, and the loading modal is hidden.
7. **Finalization**: `finalizeSetup()` creates the p5 instances and `resetVisualization()` is called to display the first frame.
**State Management (`appState`):**
The `appState` object in `state.js` is the central hub. Key properties include:
- `vizData`: The large object containing all radar frames and track data.
- `isPlaying`: A boolean that controls the `animationLoop`.
- `currentFrame`: The integer index of the currently displayed radar frame.
- `videoStartDate`, `radarStartTimeMs`: Date objects used to calculate the time offset.
- `p5_instance`, `speedGraphInstance`, `zoomSketchInstance`: References to the active p5.js sketches.
### 4. Key Logic and Interaction Flows
**Playback Synchronization (`sync.js`)**: The `animationLoop` is the core. It uses `performance.now()` to create a high-resolution timer independent of the video's `timeupdate` event. It calculates what the video's `currentTime` *should* be, finds the corresponding radar frame using a binary search (`findRadarFrameIndexForTime` in `utils.js`), and periodically corrects the video's `currentTime` if it drifts.
**UI Updates (`dom.js`)**: The `updateFrame(frame, forceVideoSeek)` function is the primary entry point for changing what's on screen. It updates the frame counter, seeks the video if `forceVideoSeek` is true, and calls the `.redraw()` methods on the p5 sketches. It's called by both the `animationLoop` (for smooth playback) and by UI event listeners like the timeline slider (for seeking).
**Session Persistence (`main.js` & `db.js`)**: On `DOMContentLoaded`, the app checks `localStorage` for saved filenames. It then calls `loadFreshFileFromDB()` to attempt to load the corresponding blobs from `IndexedDB`. If successful, `handleFiles()` is called with the cached blobs, bypassing the need for user file selection.
**Keyboard Shortcuts (`main.js`)**: A single `keydown` event listener on the document handles all shortcuts. It programmatically triggers `.click()` events on the corresponding DOM elements (e.g., Spacebar clicks `playPauseBtn`). It includes a check to prevent shortcuts from firing when the user is typing in an input field.

BIN
steps/favicon.png

Before

Width: 32  |  Height: 32  |  Size: 1.4 KiB

After

Width: 256  |  Height: 256  |  Size: 11 KiB

533
steps/index.html

@ -5,12 +5,25 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Radar and Video Visualizer - Timestamp Synchronized</title>
<link rel="icon" type="image/png" sizes="32x32" href="favicon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon.png?v=3.3.0" />
<!-- <script src="https://cdn.tailwindcss.com"></script> -->
<script>
// Silence Tailwind CDN production warning
(function () {
const suppress = (msg) => typeof msg === 'string' && (msg.includes('cdn.tailwindcss.com') || msg.includes('Tailwind CSS in production'));
['log', 'info', 'warn', 'error'].forEach(method => {
const original = console[method];
console[method] = function () {
if (arguments[0] && suppress(arguments[0])) return;
original.apply(console, arguments);
};
});
})();
</script>
<script src="./vendor/tailwind-cdn.js"></script>
<script>
!window.tailwind && (function() {
!window.tailwind && (function () {
var s = document.createElement('script');
s.src = 'https://cdn.tailwindcss.com';
document.head.appendChild(s);
@ -20,7 +33,7 @@
<!-- <script src="https://unpkg.com/oboe@2.1.5/dist/oboe-browser.min.js"></script> -->
<script src="./vendor/oboe.min.js"></script>
<script>
!window.oboe && (function() {
!window.oboe && (function () {
var s = document.createElement('script');
s.src = 'https://unpkg.com/oboe@2.1.5/dist/oboe-browser.min.js';
document.head.appendChild(s);
@ -30,7 +43,7 @@
<!-- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> -->
<script src="./vendor/chart.min.js"></script>
<script>
!window.Chart && (function() {
!window.Chart && (function () {
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/chart.js';
document.head.appendChild(s);
@ -41,7 +54,7 @@
<!-- <script src="https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js"></script> -->
<script src="./vendor/ag-grid-community.min.js"></script>
<script>
!window.agGrid && (function() {
!window.agGrid && (function () {
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js';
document.head.appendChild(s);
@ -51,13 +64,29 @@
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script> -->
<script src="./vendor/p5.js"></script>
<script>
!window.p5 && (function() {
!window.p5 && (function () {
var s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js';
document.head.appendChild(s);
})();
</script>
<link rel="stylesheet" href="./vendor/gridstack.min.css" />
<script src="./vendor/gridstack-all.js"></script>
<script>
!window.GridStack && (function () {
// If local GridStack for some reason didn't load, try the CDN
var s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/gridstack.js/10.1.2/gridstack-all.js';
document.head.appendChild(s);
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = 'https://cdnjs.cloudflare.com/ajax/libs/gridstack.js/10.1.2/gridstack.min.css';
document.head.appendChild(l);
})();
</script>
<script>
tailwind.config = {
darkMode: "class",
@ -177,11 +206,143 @@
}
/* --- END: CSS for Resizable/Draggable Panel --- */
/* --- Keyboard Shortcuts Modal Styling --- */
kbd {
background-color: #e2e8f0;
/* slate-200 */
border: 1px solid #cbd5e1;
/* slate-300 */
border-bottom-width: 2px;
color: #334155;
/* slate-700 */
border-radius: 0.375rem;
padding: 0.125rem 0.5rem;
font-family: monospace;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 600;
display: inline-block;
margin: 0 0.125rem;
}
/* Dark mode support for kbd */
.dark kbd {
background-color: #475569;
/* slate-600 */
border-color: #64748b;
/* slate-500 */
color: #f1f5f9;
/* slate-100 */
}
.feature-card {
/* Inherits bg-white/dark:bg-gray-800 from parent or class */
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
}
.dark .feature-card {
border-color: #4b5563;
/* gray-600 */
box-shadow: none;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 flex flex-col min-h-screen">
<header class="bg-white dark:bg-gray-800 shadow-md p-4 z-20 relative">
<!-- START SCREEN MODAL -->
<div id="start-screen-modal"
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100 dark:bg-gray-900 transition-opacity duration-300">
<!-- Header buttons on start screen -->
<div class="absolute top-4 right-4 flex items-center gap-2">
<button id="start-user-manual-btn" type="button"
class="bg-green-100 dark:bg-green-700 text-green-800 dark:text-green-100 border border-green-300 dark:border-green-600 shadow-sm hover:bg-green-200 dark:hover:bg-green-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Quick-Start<br>Guide
</button>
<button id="start-codebase-btn" type="button"
class="bg-purple-100 dark:bg-purple-700 text-purple-800 dark:text-purple-100 border border-purple-300 dark:border-purple-600 shadow-sm hover:bg-purple-200 dark:hover:bg-purple-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Codebase<br>Overview
</button>
<button id="start-changelog-btn" type="button"
class="bg-cyan-100 dark:bg-cyan-700 text-cyan-800 dark:text-cyan-100 border border-cyan-300 dark:border-cyan-600 shadow-sm hover:bg-cyan-200 dark:hover:bg-cyan-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
What's<br>New?
</button>
<button id="start-theme-toggle" type="button"
class="bg-indigo-900 dark:bg-amber-100 text-indigo-100 dark:text-amber-800 border border-indigo-700 dark:border-amber-300 shadow-sm hover:bg-indigo-800 dark:hover:bg-amber-200 active:scale-95 active:shadow-inner flex flex-row items-center gap-2 rounded-lg text-xs px-4 py-3.5 transition-all">
<svg id="start-theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg id="start-theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm2 0a1 1 0 112 0v-1a1 1 0 00-2 0v1zm-14 0a1 1 0 112 0v-1a1 1 0 00-2 0v1zm8 6a1 1 0 100 2 1 1 0 000-2zm5.657-11.657a1 1 0 010 1.414l-1.414 1.414a1 1 0 11-1.414-1.414l1.414-1.414a1 1 0 011.414 0zm-11.314 0a1 1 0 011.414 0l1.414 1.414a1 1 0 11-1.414 1.414l-1.414-1.414a1 1 0 010-1.414zm11.314 11.314a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 011.414-1.414l1.414 1.414a1 1 0 010 1.414zm-11.314 0a1 1 0 010-1.414l1.414-1.414a1 1 0 011.414 1.414l-1.414 1.414a1 1 0 01-1.414 0z"
fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</button>
</div>
<div
class="bg-white dark:bg-gray-800 rounded-2xl shadow-[0_0_50px_rgba(0,0,0,0.2)] p-10 w-full max-w-4xl text-center border border-gray-200 dark:border-gray-700 relative z-10">
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2 tracking-tight">Data Synchronizer</h1>
<p class="text-gray-500 dark:text-gray-400 mb-8 text-lg">Provide radar dataset and video parameters to initialize
workspace</p>
<div id="start-drop-zone"
class="border-4 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-20 mb-8 hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-gray-700 transition-colors cursor-pointer group">
<svg class="mx-auto h-16 w-16 text-gray-400 group-hover:text-blue-500 transition-colors mb-4" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="text-xl font-medium text-gray-700 dark:text-gray-300">Drag & Drop files here</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">Requires .json (and optionally video) format</p>
</div>
<div class="flex items-center justify-center gap-6">
<button id="start-load-json-btn"
class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-all text-lg font-semibold w-48 shadow-lg active:scale-95 active:shadow-inner flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
Select JSON
</button>
<button id="start-load-video-btn"
class="bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700 transition-all text-lg font-semibold w-48 shadow-lg active:scale-95 active:shadow-inner flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
Select Video
</button>
</div>
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
<button id="start-clear-cache-btn"
class="hover:text-red-500 dark:hover:text-red-400 transition-colors uppercase tracking-wider text-xs font-bold">Clear
Cached Session</button>
</div>
<!-- INTEGRATED LOADER UI (Hidden by default) -->
<div id="start-progress-container" class="hidden mt-6 w-full max-w-md mx-auto">
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div id="start-progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style="width: 0%"></div>
</div>
<div id="start-progress-text"
class="text-sm font-medium text-gray-700 dark:text-gray-300 mt-2 text-center animate-pulse">Initializing...
</div>
</div>
</div>
</div>
</div>
<header class="bg-white dark:bg-gray-800 shadow-md pt-0 px-4 pb-4 z-20 relative">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Radar and Video Synchronizer
</h1>
@ -192,7 +353,8 @@
<button id="fullscreen-btn" type="button"
class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 shadow-sm hover:bg-gray-200 dark:hover:bg-gray-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all">
FULLSCREEN<br>
<span class="text-xs font-mono bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 px-1.5 py-0.5 rounded">(F11)
<span
class="text-xs font-mono bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 px-1.5 py-0.5 rounded">(F11)
</span>
</button>
<button id="explorer-btn" type="button"
@ -203,24 +365,32 @@
(i)
</span>
</button>
<a href="User_Manual.html" target="_blank" id="user-manual-btn"
class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 shadow-sm hover:bg-gray-200 dark:hover:bg-gray-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
<button id="user-manual-btn" type="button"
class="bg-green-100 dark:bg-green-700 text-green-800 dark:text-green-100 border border-green-300 dark:border-green-600 shadow-sm hover:bg-green-200 dark:hover:bg-green-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Quick-Start<br>Guide
</a>
<button id="theme-toggle" type="button" class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 shadow-sm hover:bg-gray-200 dark:hover:bg-gray-600 active:scale-95 active:shadow-inner flex flex-row items-center gap-2 rounded-lg text-sm p-2.5 transition-all">
</button>
<button id="codebase-btn" type="button"
class="bg-purple-100 dark:bg-purple-700 text-purple-800 dark:text-purple-100 border border-purple-300 dark:border-purple-600 shadow-sm hover:bg-purple-200 dark:hover:bg-purple-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Codebase<br>Overview
</button>
<button id="shortcuts-btn" type="button"
class="bg-blue-100 dark:bg-blue-700 text-blue-800 dark:text-blue-100 border border-blue-300 dark:border-blue-600 shadow-sm hover:bg-blue-200 dark:hover:bg-blue-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Keyboard<br>Shortcuts
</button>
<button id="changelog-btn" type="button"
class="bg-cyan-100 dark:bg-cyan-700 text-cyan-800 dark:text-cyan-100 border border-cyan-300 dark:border-cyan-600 shadow-sm hover:bg-cyan-200 dark:hover:bg-cyan-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
What's<br>New?
</button>
<button id="theme-toggle" type="button"
class="bg-indigo-900 dark:bg-amber-100 text-indigo-100 dark:text-amber-800 border border-indigo-700 dark:border-amber-300 shadow-sm hover:bg-indigo-800 dark:hover:bg-amber-200 active:scale-95 active:shadow-inner flex flex-row items-center gap-2 rounded-lg text-xs px-4 py-3.5 transition-all">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm-.707 12.122l.707-.707a1 1 0 011.414 1.414l-.707.707a1 1 0 01-1.414-1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm2 0a1 1 0 112 0v-1a1 1 0 00-2 0v1zm-14 0a1 1 0 112 0v-1a1 1 0 00-2 0v1zm8 6a1 1 0 100 2 1 1 0 000-2zm5.657-11.657a1 1 0 010 1.414l-1.414 1.414a1 1 0 11-1.414-1.414l1.414-1.414a1 1 0 011.414 0zm-11.314 0a1 1 0 011.414 0l1.414 1.414a1 1 0 11-1.414 1.414l-1.414-1.414a1 1 0 010-1.414zm11.314 11.314a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 011.414-1.414l1.414 1.414a1 1 0 010 1.414zm-11.314 0a1 1 0 010-1.414l1.414-1.414a1 1 0 011.414 1.414l-1.414 1.414a1 1 0 01-1.414 0z"
fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
<span
class="text-xs font-mono bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 px-1.5 py-0.5 rounded">
(q)
</span>
</button>
</svg> </button>
</div>
</header>
@ -259,7 +429,7 @@
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-ego-speed"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked /> Show Ego Speed</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-frame-norm"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Per-Frame SNR</label>
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked /> Dynamic SNR</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-debug-overlay"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Show Debug Info</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
@ -272,6 +442,9 @@
(P)</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-covariance"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Show Covariance</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-vehicle-dimensions" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
checked /> Show Dimensions</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-confirmed-only" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
checked />
@ -328,10 +501,6 @@
</div>
</aside>
<div id="zoom-panel"
class="hidden fixed bottom-11 right-5 h-[50%] w-1/2 bg-white dark:bg-gray-800 z-20 shadow-2xl border-l-2 border-t-2 border-gray-300 dark:border-gray-600">
<div id="zoom-canvas-container" class="w-full h-full"></div>
</div>
<!-- Open Menu Button -->
<button id="toggle-menu-btn"
class="fixed top-20 left-3 z-20 bg-white dark:bg-gray-700 p-2 rounded-md shadow-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-all">
@ -345,18 +514,37 @@
<!-- END: ADD THIS NEW SCRIM ELEMENT -->
<!-- Main Content -->
<main class="flex-grow container mx-auto p-4 pt-8 flex flex-col lg:flex-row gap-6">
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="relative">
<div id="canvas-container"
class="w-full h-[75vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center">
<p id="canvas-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load JSON data to start
<main class="flex-grow w-full max-w-[1920px] mx-auto p-4 pt-8">
<div class="grid-stack grid-stack-12">
<!-- Radar Panel -->
<div class="grid-stack-item shadow-md rounded-lg" gs-id="radar-panel" gs-x="0" gs-y="0" gs-w="6" gs-h="12">
<div
class="grid-stack-item-content bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg overflow-visible flex flex-col relative group">
<div
class="absolute top-0 left-0 w-full h-4 cursor-grab active:cursor-grabbing bg-transparent hover:bg-gray-300/30 z-[60] transition-colors rounded-t-lg"
title="Drag to move panel"></div>
<div class="relative w-full h-full flex-grow">
<div id="canvas-container" class="w-full h-full flex items-center justify-center relative">
<p id="canvas-placeholder" class="hidden text-gray-500 dark:text-gray-400 text-lg">Load JSON data to start
visualization</p>
<!-- Range Slider (Vertical) -->
<div class="absolute bottom-8 left-2 flex flex-col items-center gap-2 z-20 group">
<span id="range-value-display"
class="bg-black bg-opacity-60 text-white text-[10px] px-1.5 py-0.5 rounded font-mono">80m</span>
<input type="range" id="range-slider" min="40" max="200" value="80" step="10"
class="h-32 cursor-pointer accent-blue-600"
style="writing-mode: vertical-lr; direction: rtl; width: 8px;" />
<span class="text-[10px] font-bold text-gray-400 uppercase tracking-tighter"
style="writing-mode: vertical-lr; transform: rotate(180deg);">Range</span>
</div>
</div>
<div id="radar-info-overlay"
class="absolute top-1 left-2 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden">
class="absolute top-4 left-0 right-0 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden pointer-events-none">
</div>
<div class="absolute bottom left-1/2 -translate-x-1/2 flex items-center justify-center gap-4">
<div
class="absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center justify-center gap-4 z-20 pointer-events-none">
<div id="ego-speed-display"
class="bg-black bg-opacity-60 text-white text-sm px-3 py-1.5 rounded-md hidden font-mono"></div>
<div id="can-speed-display"
@ -364,33 +552,58 @@
</div>
</div>
</div>
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="w-full h-[50vh] bg-black rounded-lg shadow-inner flex items-center justify-center relative">
</div>
<!-- Video Panel -->
<div class="grid-stack-item shadow-md rounded-lg" gs-id="video-panel" gs-x="6" gs-y="0" gs-w="6" gs-h="8">
<div
class="grid-stack-item-content bg-black rounded-lg overflow-hidden flex items-center justify-center relative group">
<div
class="absolute top-0 left-0 w-full h-4 cursor-grab active:cursor-grabbing bg-transparent hover:bg-white/20 z-[60] transition-colors rounded-t-lg"
title="Drag to move panel"></div>
<video id="video-player" class="w-full h-full object-contain hidden" muted playsinline></video>
<p id="video-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load a video file</p>
<p id="video-placeholder" class="hidden text-gray-500 text-lg">Load a video file</p>
<div id="video-info-overlay"
class="absolute top-1 left-2 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden">
class="absolute top-4 left-2 z-[20] bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden pointer-events-none">
</div>
<div id="debug-overlay"
class="absolute top-0 left-0 bg-black bg-opacity-60 text-white p-2 font-mono text-xs hidden w-full"></div>
class="absolute top-0 left-0 bg-black bg-opacity-60 text-white p-2 font-mono text-xs hidden w-full z-[20] pointer-events-none">
</div>
<div id="speed-graph-container"
class="w-full h-[25vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center">
<p id="speed-graph-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load JSON & Video to see speed
graph
</p>
</div>
</div>
<div id="data-explorer-panel"
class="hidden fixed bottom-24 right-4 bg-white dark:bg-gray-800 shadow-2xl rounded-lg z-30 flex flex-col w-full max-w-2xl h-1/2 border dark:border-gray-600">
<!-- START: Add Draggable Header and Resizer Handles -->
<div id="data-explorer-header"
class="flex items-center justify-between p-2 border-b dark:border-gray-700 cursor-move">
<h2 class="text-lg font-bold ml-2">Data Explorer</h2>
<button id="close-explorer-btn"
class="text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg p-1.5">&times;</button>
<!-- SpeedGraph Panel -->
<div class="grid-stack-item shadow-md rounded-lg" gs-id="speed-graph-panel" gs-x="6" gs-y="8" gs-w="6" gs-h="4">
<div
class="grid-stack-item-content bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden flex items-center justify-center relative group">
<div
class="absolute top-0 left-0 w-full h-4 cursor-grab active:cursor-grabbing bg-transparent hover:bg-gray-300/30 z-[60] transition-colors rounded-t-lg"
title="Drag to move panel"></div>
<div id="speed-graph-container" class="w-full h-full flex items-center justify-center p-1 relative">
<p id="speed-graph-placeholder"
class="text-gray-500 dark:text-gray-400 text-lg text-center absolute pointer-events-none">Load JSON &
Video to see speed graph</p>
</div>
</div>
</div>
</div>
<!-- START: Floating Zoom Panel -->
<div id="zoom-panel"
class="hidden fixed bottom-11 right-5 bg-white dark:bg-gray-800 shadow-2xl rounded-lg z-30 flex flex-col border dark:border-gray-600"
style="width: 900px; height: 455px;">
<div id="zoom-panel-header"
class="flex items-center justify-between p-2 border-b dark:border-gray-700 cursor-move bg-gray-100 dark:bg-gray-700 rounded-t-lg">
<h2 class="text-sm font-bold text-gray-500 uppercase tracking-wider ml-2 pointer-events-none">God Mode</h2>
<button id="close-zoom-btn"
class="text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg p-1 px-2">&times;</button>
</div>
<div class="resizer resizer-t"></div>
<div class="resizer resizer-r"></div>
<div class="resizer resizer-b"></div>
@ -399,16 +612,38 @@
<div class="resizer resizer-tr"></div>
<div class="resizer resizer-br"></div>
<div class="resizer resizer-bl"></div>
<div id="zoom-canvas-container"
class="flex-grow relative flex items-center justify-center overflow-hidden rounded-b-lg">
</div>
</div>
<!-- END: Floating Zoom Panel -->
<div id="data-explorer-panel"
/* flex-col is critical for the header/tabs/content stack */
class="hidden fixed bottom-24 right-4 bg-white dark:bg-gray-800 shadow-2xl rounded-lg z-30 flex flex-col w-full max-w-2xl h-1/2 border dark:border-gray-600">
<!-- START: Add Draggable Header and Resizer Handles -->
<!-- Draggable Header: flex-shrink-0 prevents it from being squashed during vertical resize -->
<div id="data-explorer-header"
class="flex items-center justify-between p-2 border-b dark:border-gray-700 cursor-move flex-shrink-0">
<h2 class="text-lg font-bold ml-2">Data Explorer</h2>
<button id="close-explorer-btn"
class="text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg p-1.5">&times;</button>
</div>
<!-- END: Add Draggable Header and Resizer Handles -->
<div class="flex border-b dark:border-gray-700">
<button id="tab-btn-tree" class="px-4 py-2 text-sm font-medium border-b-2 border-blue-500">Tree View</button>
<button id="tab-btn-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Grid
<!-- Scrollable Tab Row: overflow-x-auto allows navigation at narrow widths. flex-shrink-0 is required to keep it visible. -->
<div class="flex border-b dark:border-gray-700 overflow-x-auto overflow-y-hidden whitespace-nowrap flex-shrink-0">
<button id="tab-btn-tree" class="px-4 py-2 text-sm font-medium border-b-2 border-blue-500 flex-shrink-0">Tree View</button>
<button id="tab-btn-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">Grid
View</button>
<button id="tab-btn-track-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Track
<button id="tab-btn-track-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">Track
Grid</button>
<button id="tab-btn-plot" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Plot
<button id="tab-btn-adas" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">ADAS
Data</button>
<button id="tab-btn-plot" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">Plot
View</button>
</div>
@ -420,6 +655,9 @@
<div id="tab-panel-track-grid" class="hidden h-full p-2">
<div id="track-data-grid" class="ag-theme-alpine-dark h-full w-full"></div>
</div>
<div id="tab-panel-adas" class="hidden h-full p-2 overflow-y-auto">
<div id="adas-vertical-view" class="space-y-4"></div>
</div>
<div id="tab-panel-plot" class="hidden p-2">
<canvas id="data-chart"></canvas>
</div>
@ -429,6 +667,16 @@
<button id="plot-selected-btn"
class="bg-blue-600 text-white px-3 py-1 rounded-lg text-sm hover:bg-blue-700">Plot Selected Column</button>
</div>
<!-- Resizers -->
<div class="resizer resizer-t"></div>
<div class="resizer resizer-r"></div>
<div class="resizer resizer-b"></div>
<div class="resizer resizer-l"></div>
<div class="resizer resizer-tl"></div>
<div class="resizer resizer-tr"></div>
<div class="resizer resizer-br"></div>
<div class="resizer resizer-bl"></div>
</div>
</main>
@ -483,6 +731,160 @@
</div>
</footer>
<!-- SHORTCUTS MODAL -->
<div id="shortcuts-modal"
class="fixed inset-0 z-50 flex items-center justify-center hidden bg-black bg-opacity-80 backdrop-blur-sm p-4">
<div
class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-y-auto relative flex flex-col">
<!-- Header -->
<div
class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-900 z-10">
<div class="flex items-center gap-3">
<span class="text-3xl">⌨️</span>
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Keyboard Shortcuts</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">Press <kbd>ESC</kbd> or click outside to close</p>
</div>
</div>
<button id="shortcuts-modal-close-btn"
class="px-6 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 font-semibold text-lg transition-colors">
Skip
</button>
</div>
<!-- Content Grid -->
<div class="p-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Playback -->
<div class="feature-card p-6 bg-gray-50 dark:bg-gray-800">
<h3
class="mb-4 text-lg font-bold text-blue-600 dark:text-blue-400 border-b pb-2 border-gray-200 dark:border-gray-700">
Playback & Navigation</h3>
<ul class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<li class="flex justify-between items-center"><span>Play / Pause</span> <kbd>Space</kbd></li>
<li class="flex justify-between items-center"><span>Next Frame</span> <kbd></kbd></li>
<li class="flex justify-between items-center"><span>Prev Frame</span> <kbd></kbd></li>
<li class="flex justify-between items-center"><span>Video Frame Step</span> <kbd></kbd> / <kbd></kbd></li>
<li class="flex justify-between items-center"><span>Scroll Seek</span> <kbd>Mouse Wheel</kbd></li>
<li class="flex justify-between items-center"><span>Reset Frame 0</span> <kbd>R</kbd></li>
</ul>
</div>
<!-- View & Toggles -->
<div class="feature-card p-6 bg-gray-50 dark:bg-gray-800">
<h3
class="mb-4 text-lg font-bold text-emerald-600 dark:text-emerald-400 border-b pb-2 border-gray-200 dark:border-gray-700">
View & UI Toggles</h3>
<ul class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<li class="flex justify-between items-center"><span>Sidebar Menu</span> <kbd>M</kbd></li>
<li class="flex justify-between items-center"><span>Data Explorer</span> <kbd>I</kbd></li>
<li class="flex justify-between items-center"><span>GOD MODE Zoom</span> <kbd>G</kbd></li>
<li class="flex justify-between items-center"><span>Dark/Light Theme</span> <kbd>Q</kbd></li>
<li class="flex justify-between items-center"><span>Adv. Debug Info</span> <kbd>A</kbd></li>
<li class="flex justify-between items-center"><span>Fullscreen</span> <kbd>F11</kbd></li>
</ul>
</div>
<!-- Data Display -->
<div class="feature-card p-6 bg-gray-50 dark:bg-gray-800">
<h3
class="mb-4 text-lg font-bold text-pink-600 dark:text-pink-400 border-b pb-2 border-gray-200 dark:border-gray-700">
Data Visualization</h3>
<ul class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<li class="flex justify-between items-center"><span>Toggle Tracks</span> <kbd>T</kbd></li>
<li class="flex justify-between items-center"><span>Object Details</span> <kbd>D</kbd></li>
<li class="flex justify-between items-center"><span>Predicted Pos</span> <kbd>P</kbd></li>
<li class="flex justify-between items-center"><span>Confirmed Only</span> <kbd>C</kbd></li>
<li class="flex justify-between items-center"><span>Color by SNR</span> <kbd>1</kbd></li>
<li class="flex justify-between items-center"><span>Color by Cluster</span> <kbd>2</kbd></li>
<li class="flex justify-between items-center"><span>Color by Inlier</span> <kbd>3</kbd></li>
<li class="flex justify-between items-center"><span>Color by Stationary</span> <kbd>4</kbd></li>
</ul>
</div>
</div>
<!-- Footer Tip -->
<div
class="p-6 bg-gray-50 dark:bg-gray-800 text-center border-t border-gray-200 dark:border-gray-700 rounded-b-2xl">
<p class="text-sm text-gray-500">Pro Tip: You can also press <kbd>K</kbd> anytime to toggle this screen.</p>
</div>
</div>
</div>
<!-- USER GUIDE MODAL -->
<div id="guide-modal"
class="fixed inset-0 z-50 flex items-center justify-center hidden bg-black bg-opacity-80 backdrop-blur-sm p-4">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full h-full mx-8 my-4 relative flex flex-col">
<!-- Header -->
<div
class="flex items-center justify-between px-6 py-1 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-900 z-10 rounded-t-2xl">
<div class="flex items-center gap-3">
<span class="text-3xl">📚</span>
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">User Manual</h2>
</div>
</div>
<button id="guide-modal-close-btn"
class="px-6 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 font-semibold text-lg">Skip</button>
</div>
<!-- Iframe Content -->
<iframe id="user-manual-iframe" src="annex/User_Manual.html?v=3.3.0"
class="flex-grow w-full border-0 rounded-b-2xl" style="min-height: 400px;"></iframe>
</div>
</div>
<!-- CODEBASE OVERVIEW MODAL -->
<div id="codebase-modal"
class="fixed inset-0 z-50 flex items-center justify-center hidden bg-black bg-opacity-80 backdrop-blur-sm p-4">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full h-full mx-8 my-4 relative flex flex-col">
<!-- Header -->
<div
class="flex items-center justify-between px-6 py-1 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-900 z-10 rounded-t-2xl">
<div class="flex items-center gap-3">
<span class="text-3xl"></span>
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white"></h2>
</div>
</div>
<button id="codebase-modal-close-btn"
class="px-6 py-2 rounded-lg bg-purple-600 text-white hover:bg-purple-700 font-semibold text-lg">Close</button>
</div>
<!-- Iframe Content -->
<iframe id="codebase-iframe" src="annex/code-base-overview.html?v=3.3.0"
class="flex-grow w-full border-0 rounded-b-2xl" style="min-height: 400px;"></iframe>
</div>
</div>
<!-- CHANGELOG MODAL -->
<div id="changelog-modal"
class="fixed inset-0 z-50 flex items-center justify-center hidden bg-black bg-opacity-80 backdrop-blur-sm p-4">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full h-full mx-8 my-4 relative flex flex-col">
<!-- Header -->
<div
class="flex items-center justify-between px-6 py-1 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-900 z-10 rounded-t-2xl">
<div class="flex items-center gap-3">
<span class="text-3xl">🚀</span>
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Changelog</h2>
</div>
</div>
<button id="changelog-modal-close-btn"
class="px-6 py-2 rounded-lg bg-cyan-600 text-white hover:bg-cyan-700 font-semibold text-lg">Close</button>
</div>
<!-- Iframe Content -->
<iframe id="changelog-iframe" src="annex/Changelog_3.3.0.html" class="flex-grow w-full border-0 rounded-b-2xl"
style="min-height: 400px;"></iframe>
</div>
</div>
<div id="modal-container" class="fixed inset-0 z-50 flex items-center justify-center hidden">
<div id="modal-overlay" class="absolute inset-0 bg-black bg-opacity-50"></div>
<div id="modal-content"
@ -495,7 +897,7 @@
<span id="modal-progress-text" class="text-sm text-gray-500 dark:text-gray-400 mt-1 block text-center"></span>
<div class="flex justify-end gap-4 mt-4">
<button id="modal-cancel-btn"
class="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold">Cancel</button>
class="px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 font-semibold">Cancel</button>
<button id="modal-ok-btn"
class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 font-semibold">OK</button>
</div>
@ -505,7 +907,24 @@
<input type="file" id="json-file-input" class="hidden" accept=".json" />
<input type="file" id="video-file-input" class="hidden" accept="video/*" />
<input type="file" id="session-file-input" class="hidden" accept=".json" />
<script type="module" src="./src/main.js"></script>
<!-- Global drag-and-drop overlay -->
<div id="global-drag-overlay"
class="fixed inset-0 z-[60] flex items-center justify-center bg-blue-600/30 backdrop-blur-sm border-8 border-dashed border-blue-500 rounded-3xl m-4 pointer-events-none opacity-0 transition-opacity duration-300">
<div
class="bg-blue-600 text-white px-10 py-5 rounded-2xl shadow-2xl flex items-center gap-6 border-4 border-blue-400">
<svg class="h-16 w-16 animate-bounce" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<div class="text-left">
<span class="text-4xl font-extrabold block">Drop to Load Data</span>
<span class="text-lg font-medium opacity-90">Updating session with new files...</span>
</div>
</div>
</div>
<script type="module" src="./src/main.js?v=3.3.0"></script>
</body>
</html>

66
steps/intel/GEMINI.md

@ -0,0 +1,66 @@
# ARAS Radar and Video Synchronizer
High-precision, browser-based tool for visualizing radar point cloud data, object tracks, and CAN bus speed data, synchronized with a corresponding video file.
## Project Overview
This project is a modular ES6 JavaScript application refactored from a monolithic codebase. It provides a sophisticated interface for multi-sensor data playback and analysis.
### Core Technologies
- **Rendering**: [p5.js](https://p5js.org/) for main radar, speed graph, and "GOD MODE" visualizations. Hardened with Triple-Buffer protection.
- **Parsing**: Web Workers using [Clarinet.js](https://github.com/dscape/clarinet) for non-blocking, streaming JSON parsing of large datasets.
- **Storage**: **IndexedDB** for persistent file caching; **localStorage** for user workspace state and layout persistence.
- **Layout Engine**: [GridStack.js](https://gridstackjs.com/) for the modular, resizable dashboard interface.
- **Styling**: Tailwind CSS for a premium, dark-mode-first UI.
- **Data Exploration**: [AG Grid](https://www.ag-grid.com/), **Custom Vertical Property View**, and [Chart.js](https://www.chartjs.org/) for forensic data inspection.
### Architecture
- **State Management**: Centralized in `src/state.js` via the `appState` object.
- **Synchronization**: `src/sync.js` uses high-resolution `performance.now()` to perfectly align radar frames with video playback.
- **Workspace Engine**: `src/ui.js` manages a Hybrid Dashboard (GridStack + Standalone Floating Windows) with auto-focus and viewport rescue logic.
- **Modular Design**: Functional decomposition into specialized modules for Sync, Data, UI, and Visualizations.
## Building and Running
The project is designed to run as a static web application but requires a local server due to security restrictions on file access and Web Workers.
### Prerequisites
- Python 3.x installed on your system.
### Quick Start
1. **Check Environment**: Run `python_check.bat` to verify Python is in your PATH.
2. **Start Server**: Run `Visualization_Start.bat`. This executes `python -m http.server 8000`.
3. **Access App**: Open your browser and navigate to `http://localhost:8000`.
### Development
Since this is a static project using ES6 modules directly in the browser:
- No build step (e.g., Webpack/Vite) is currently required for basic usage.
- Tests can be run by opening `tests/test-runner.html` in a local server environment.
## Key Directories and Files
- `src/`: Main source code directory.
- `p5/`: p5.js sketches for Radar, Speed Graph, and Standalone "GOD MODE" Zoom.
- `dataExplorer.js`: Logic for the Data Explorer panel (AG Grid, Vertical Property View, Chart.js).
- `ui.js`: Unified UI/Workspace engine with layout memory and resizable panel logic.
- `sync.js`: High-precision synchronization logic.
- `parser.worker.js`: Off-thread streaming JSON parser.
- `vendor/`: Local copies of 3rd party libraries ensuring offline functionality.
- `annex/`: Technical guides, shortcuts, and infographics for the dashboard.
- `intel/`: Project documentation and high-level architecture guides (this folder).
- `Data_structs/`: Documentation for JSON and ROS2 sensor streams.
## Development Conventions
### Coding Style
- **Modularization**: Follow the established pattern of separating logic into `src/` modules. Avoid adding logic to `index.html`.
- **State Access**: Always use `appState` for reactive data.
- **Drawing**: Use `src/drawUtils.js` for reusable drawing functions shared between `radarSketch` and `zoomSketch`.
### Naming
- Use camelCase for variables and functions.
- Use PascalCase for class-like structures (though the project primarily uses objects and functions).
### Documentation
- Maintain `readme.md`, `GEMINI.md`, and `context.md` in the `intel/` folder with significant architectural changes.
- Use `Improvements.txt` (in `annex/`) to track progress on the refactor and new feature requests.
- Reference supplementary guides in `annex/` (User Manual, Shortcuts, Changelog).

143
steps/intel/context.md

@ -0,0 +1,143 @@
Context Document: Radar and Video Synchronizer Application
### 1. High-Level Overview
**Core Purpose**: A high-precision, browser-based tool for visualizing and synchronizing radar sensor data (JSON) with a corresponding video file. It allows for detailed analysis of object tracks, point clouds, and vehicle dynamics.
**Core Technologies**:
- **Frontend**: HTML5, Tailwind CSS
- **Logic**: Modular JavaScript (ES6 Modules)
- **Visualization**: `p5.js` for the main radar plot, a zoomed-in "god mode" view, and a time-series speed graph.
- **Data Handling**:
- **Web Workers** (`parser.worker.js`) with the `Clarinet.js` streaming library to parse large JSON files off the main thread, preventing UI freezes.
- `Oboe.js` is included in vendor files but `Clarinet.js` is the active parser.
- **Data Exploration**:
- `AG-Grid` for tabular Point Cloud and Track data.
- **Vertical Property View** (`dataExplorer.js`) for high-density ADAS data.
- `Chart.js` for plotting data from the grids.
- **Persistence**: `IndexedDB` for caching large files (JSON, Video) and `localStorage` for user settings (UI state, theme, file references).
### 2. Project Architecture & File Structure
The application uses a modular ES6 structure. All source code resides in the `/src` directory.
- **`index.html`**: The main HTML shell. Defines the DOM structure, including the main layout, collapsible sidebar, data explorer panel, and documentation modals (User Manual, Shortcuts, Codebase Overview, Changelog). It loads all necessary CDN libraries and the main JS module.
...
- **`/annex/`**: **Supplementary Documentation**. Contains HTML files for the User Manual, Keyboard Shortcuts, Codebase Overview, and Changelog, as well as the progress log (`Improvements.txt`).
- **`/intel/`**: **Project Intelligence**. Contains documentation and AI-specific context files (`readme.md`, `GEMINI.md`, `context.md`).
- **`/src/main.js`**: **The Orchestrator**. This is the application's entry point. It initializes the database, theme, and data explorer. It sets up the initial event listeners for the UI and delegates specialized tasks to other modules (`fileLoader.js`, `keyboard.js`, `sync.js`, `ui.js`).
- **`/src/state.js`**: **The Single Source of Truth**. Exports a single global `appState` object that holds all dynamic data (e.g., `vizData`, `isPlaying`, `currentFrame`). All modules import from this file.
- **`/src/fileLoader.js`**: **File Management**. Handles the file loading pipeline. It manages drag-and-drop interactions, file input changes, and the unified processing of JSON and Video files. It handles caching logic (saving/loading from `IndexedDB`) and triggers the parsing worker.
- **`/src/keyboard.js`**: **Input Handling**. Manages all keyboard shortcuts. It creates a centralized `keydown` listener that triggers UI actions (play/pause, seeking, toggles) and prevents interference with input fields.
- **`/src/sync.js`**: **The Heartbeat & Controller**. Contains the core logic for playback and synchronization.
- `animationLoop()`: The main render loop that keeps the UI updated.
- `videoFrameCallback()`: The high-precision video timing loop.
- `updateFrame()`: The central function that updates the current frame index, synchronizes the video (handling drift), and updates UI elements (sliders, counters, overlays).
- `resetVisualization()`: Resets the playback state.
- Handles timeline interactions (input, scroll wheel) and video panel scrolling.
- **`/src/dom.js`**: **The UI Interface**. Exports constants for every key DOM element. It contains functions to update specific UI components like persistent overlays (`updatePersistentOverlays`), debug overlays (`updateDebugOverlay`), and custom TTC scheme inputs. *Note: Core playback-driven UI updates have moved to `sync.js`.*
- **`/src/fileParsers.js`**: **The Data Processor**. Contains `parseVisualizationJson()`, which takes the raw parsed JSON object and enriches it with calculated `timestampMs` values relative to the video start time and determines global SNR ranges.
- **`/src/parser.worker.js`**: **The Heavy Lifter**. A Web Worker that uses `Clarinet.js` to stream-parse the JSON file, preventing the main thread from freezing. It posts progress updates and the final parsed object back to `main.js` (via `fileLoader.js`).
- **`/src/db.js`**: **The Caching Layer**. Manages all interactions with `IndexedDB` to save and load file blobs and their metadata, enabling fast session reloads.
- **`/src/dataExplorer.js`**: **The Inspector**. Manages the "Data Explorer" panel.
- **Standard Grids**: Uses AG-Grid for Point Cloud and Track data.
- **ADAS View**: Implements a custom Vertical Property View for readable ADAS inspection.
- **Visualization**: Chart.js for plotting selected numeric columns.
- **Optimizations**: `throttledUpdateExplorer` for performance; scrollable tabs and 250x200 minimum dimensions for compact layouts.
- **`/src/ui.js`**: **The UI Engine**. Manages all advanced interface interactions.
- `makeDraggableAndResizable()`: A unified utility that transforms any DOM element into a floating window with persistent memory.
- **Memory & Persistence**: Saves/Loads panel coordinates and GridStack layouts to `localStorage`.
- **Viewport Rescue**: Automatically recovers off-screen windows during browser resizing.
- **Auto-Focus**: Dynamically manages `z-index` to bring active windows to the front on click.
- **`/src/debug.js`**: **Debug Configuration**. Exports `debugFlags` to toggle logging for various subsystems (sync, drawing, file loading) and configure constants like video load timeouts.
- **`/src/utils.js`**: **Toolbox**. A collection of pure, reusable helper functions (e.g., `findRadarFrameIndexForTime` (binary search), timestamp parsers, `throttle`, `precomputeRadarVideoSync`).
- **`/src/modal.js`**: **User Feedback**. Manages the logic for pop-up modal dialogs, including notifications, confirmations, and loading progress bars.
- **`/src/theme.js`**: **Styling**. Handles the dark/light mode theme switching.
- **`/src/constants.js`**: **Configuration**. Stores shared, static values like `VIDEO_FPS` and radar plot boundaries.
- **`/src/p5/`**: **Visualization Modules**.
- **`radarSketch.js`**: Master timer for God Mode auto-visibility (5s delay). Hardened with guards against 0-width crashes.
- **`zoomSketch.js`**: "GOD MODE" view. Includes the "Closing in..." countdown (3s) and "Out of Bounds" safety indicators.
- **`speedGraphSketch.js`**: Refined with initialization guards to handle rapid layout shifts.
- **`/src/drawUtils.js`**: **The Artist's Toolkit**. Contains pure drawing functions called by the p5 sketches (e.g., `drawPointCloud`, `drawTrajectories`). This is where the visual appearance of radar objects is defined.
### 3. Data Flow & State Management
**File Loading Pipeline (`fileLoader.js`):**
1. **Input**: User drops files or clicks load buttons. `handleFiles()` identifies the file types.
2. **Processing (`processFilePipeline`)**: A loading modal is shown.
3. **Caching**: Files are saved to `IndexedDB` (non-blocking).
4. **Offset**: Timestamp offset is calculated from filenames.
5. **JSON Parsing**: JSON is sent to `parser.worker.js`. The worker streams the file and returns the object.
6. **Post-Processing**: `parseVisualizationJson()` (in `fileParsers.js`) enriches the data. `precomputeRadarVideoSync()` (in `utils.js`) bakes sync times.
7. **Video Loading**: The video is loaded into the player.
8. **Finalization**: `finalizeSetup()` (in `fileLoader.js`) resets the visualization, creates/updates p5 sketches, and updates the UI.
**Playback & Synchronization (`sync.js`):**
- **Video Master**: The video element's time is the source of truth when playing.
- **`videoFrameCallback`**: Runs on every video frame, finds the corresponding radar frame index, and updates `appState.currentFrame`.
- **`animationLoop`**: Runs on `requestAnimationFrame` (~60Hz). It calls `updateFrame()` to reflect the state on the screen.
- **`updateFrame()`**:
- Updates UI (slider, counter).
- Updates overlays (Ego speed, CAN speed).
- Calls `throttledUpdateExplorer`.
- Handles Video Seek: If `forceVideoSeek` is true (e.g., user scrubbed the timeline), it sets `videoPlayer.currentTime`.
- **Drift Correction**: If the video drifts significantly from the target radar frame time, `updateFrame` forces a seek to resync.
**State Management (`appState`):**
The `appState` object in `state.js` is the central hub. Key properties include:
- `vizData`: The large object containing all radar frames and track data.
- `isPlaying`: Boolean controlling the loop.
- `currentFrame`: Integer index of the currently displayed radar frame.
- `offset`: Manual or auto-calculated time offset (ms).
- `videoStartDate`, `radarStartTimeMs`: Timestamps for absolute time calculation.
- `p5_instance`, `speedGraphInstance`: References to active sketches.
- `videoMissing`: Flag for JSON-only mode.
### 4. Key Interaction Flows
**Timeline Scrubbing (`sync.js`)**:
- **Drag**: `handleTimelineInput` updates the UI immediately for responsiveness but debounces the expensive video seek until the user stops dragging.
- **Scroll Wheel**: `handleTimelineWheel` allows frame-by-frame or accelerated seeking. It also updates UI immediately and debounces the video seek.
**Data Explorer (`dataExplorer.js`)**:
- Activated by `I` key or canvas click.
- Shows Tree, Grid, and Plot views.
- Updates are throttled to prevent performance degradation during playback.
**God Mode Auto-Hide (`p5/*.js`)**:
- **Sequence**: The panel appears on hover. If the mouse stops moving over a relevant point, a 5-second "Analysis Period" begins. After 5s, a visual 3-second countdown appears. Total 8s before hiding.
- **Override**: Any new interaction or mouse movement resets the full 8-second timer.
**Layout Persistence (`ui.js` & `main.js`)**:
- **GridStack**: Uses a "Soft Restore" loop to update panel positions by ID, ensuring p5 canvases and Video elements are not destroyed during layout changes.
- **Floating Panels**: Tracks `top/left/width/height` individually per panel ID.
- **Nuclear Reset**: The "Clear Cache and Reload" button wipes all UI memory, returning the app to factory defaults.
**Session Management (`main.js` & `db.js`)**:
- `saveSessionBtn` saves current filenames, offset, and toggles to a JSON file.
- `loadSessionBtn` reads the session file. It verifies that the referenced files exist in `IndexedDB` (via `loadFreshFileFromDB`) before applying settings and reloading the page.
**Keyboard Shortcuts (`keyboard.js`)**:
- Centralized handler for `Play/Pause` (Space), `Seek` (Arrows), `Toggle Views` (1-4, T, D, G, P, C), `Debug` (A), `Theme` (Q).
- Smartly ignores shortcuts when input fields are focused.

146
steps/intel/readme.md

@ -0,0 +1,146 @@
# Radar and Video Synchronizer (Refactored)
**Version**: 3.3.0 (Synchronized Workspace Update)
## 🎯 Overview
This is a high-precision, browser-based tool for visualizing radar point cloud data, object tracks, and CAN bus speed data, synchronized with a corresponding video file. Originally a monolithic HTML application, it has been refactored into a modern, modular JavaScript application using ES6 Modules.
The application leverages **p5.js** for rendering, **Web Workers** for background streaming JSON parsing, **IndexedDB** for persistent file caching, and **Tailwind CSS** for a professional, responsive UI.
---
## ⚙️ Core Architecture
### `sync.js` (Synchronized Playback)
- **Master Clock**: Utilizes `performance.now()` for a high-resolution timer independent of imprecise video events.
- **Dynamic Mapping**: Maps the target media time (adjusted by user offsets) to the corresponding radar frame via binary search (`utils.js::findRadarFrameIndexForTime`).
- **Drift Correction**: Periodically checks for sync drift (>150ms) and forces video seeks to maintain frame-perfect alignment.
### `fileLoader.js` (Unified File Loading)
- **Multi-Input**: Supports both button-based selection and global Drag & Drop.
- **Pipeline Processing**: Identifies file types and triggers the appropriate caching and parsing workers.
- **Auto-Offset**: Automatically calculates time offsets based on standardized filenames (e.g., `fHist_...json` and `WIN_...mp4`).
### `parser.worker.js` (Streaming JSON Parser)
- **Off-Thread Processing**: Uses a Web Worker to keep the UI responsive during large file loads.
- **Streaming Logic**: Utilizes the `Clarinet.js` event-driven parser to reconstruct large datasets in memory without blocking the main thread.
- **Progress Reporting**: Sends real-time percentage updates back to the UI.
---
## 🎨 Visualizations (p5.js)
### `radarSketch.js` (Primary Radar Canvas)
- **Radar Rendering**: Draws point clouds, tracks, ego-vehicle representation, and cluster centroids.
- **Interactive Layers**: Manages toggleable overlays for SNR, TTC, predicted positions, and covariance ellipses.
- **Hardening**: Features **Triple-Buffer Protection** against 0-width crashes and a master timer for God Mode auto-visibility.
### `speedGraphSketch.js` (Telemetry Graph)
- **Time-Series Analysis**: Renders Ego Speed and CAN Speed lines synchronized with the video playback.
- **Visual Indicators**: Draws a vertical tracking line aligned with the current active frame.
### `zoomSketch.js` (Magnified "GOD MODE")
- **Standalone Window [NEW]**: A decoupled, floating overlay that provides high-fidelity zoom.
- **Auto-Hide UX [NEW]**: 8-second sequence (5s analysis period + 3s visual countdown) before fading.
- **Safety Indicators**: Includes "Out of Bounds" warnings and "Closing in..." countdown status labels.
---
## 🖥️ User Interface & Workspace
### `ui.js` (Workspace & Window Engine) [NEW]
- **Hybrid Dashboard**: Manages the mix of GridStack-based fixed panels and independent floating modules.
- **Layout Persistence**: Automatically saves/restores coordinates and GridStack geometry to `localStorage`.
- **Soft-Restore**: Updates layout positions by ID, preserving canvases and video players without destructive DOM replacement.
- **Viewport Rescue**: Automatically pulls off-screen panels back into view during window resize events.
- **Auto-Focus**: Clicking any floating panel dynamically increases its z-index (bringing it to the front).
### `dataExplorer.js` (Data Inspector)
- **Deep Inspection**: Provides Tree View, AG-Grid View, and Chart.js Plotting for raw numerical frame data.
- **ADAS Property View [NEW]**: A specialized Vertical Property View for ADAS data, optimized for high readability of many columns in narrow panels.
- **Persistent Memory [NEW]**: Remembers its last position, size, and display state across page reloads.
- **Layout Optimization [NEW]**: Support for compact "sidecar" mode with reduced minimum dimensions (250x200) and scrollable navigation tabs.
### `keyboard.js` (Shortcuts)
- **Centralized Handler**: Manages all keyboard interactions (Space, 1-4, S, T, D, G, P, etc.).
- **Smart Focus**: Prevents shortcuts from firing when the user is typing in number or text inputs.
---
## 📂 Project Structure
```text
├── index.html # Main HTML shell
├── Visualization_Start.bat # Script to start the local server
├── python_check.bat # Script to check Python installation
├── favicon.png # Browser tab icon
├── package-lock.json # NPM lockfile
├── annex/ # Supplementary documentation and reference files
│ ├── User_Manual.html # Content for the Quick Start Guide (loaded via iframe)
│ ├── code-base-overview.html # Technical overview of the codebase (infographic style)
│ ├── Changelog_3.3.0.html # Current version release notes (loaded via iframe)
│ ├── Changelog.html # Legacy change archive
│ └── shortcuts.html # Reference for keyboard shortcuts (legacy/static)
├── intel/ # Project documentation and AI context
│ ├── readme.md # This documentation
│ ├── context.md # Detailed technical overview for AI assistance
│ ├── GEMINI.md # High-level project overview for AI
├── src/
│ ├── constants.js # Shared constants (radar bounds, FPS)
│ ├── dataExplorer.js # Logic for the Data Explorer panel (AG Grid, Chart.js)
│ ├── db.js # IndexedDB logic for caching files
│ ├── debug.js # Debug logging flags and console exposure
│ ├── dom.js # Centralized DOM element references
│ ├── drawUtils.js # p5.js drawing helpers (points, tracks, axes, legends)
│ ├── fileLoader.js # File handling pipeline (Dropzone, Inputs, Worker trigger)
│ ├── fileParsers.js # Post-processing logic for parsed JSON
│ ├── keyboard.js # Keyboard shortcut handler
│ ├── main.js # Main application entry point, initialization logic
│ ├── modal.js # Logic for pop-up modal dialogs & progress bar
│ ├── parser.worker.js # Web Worker for background JSON parsing (uses Clarinet.js)
│ ├── session.js # NEW: Logic for saving and loading session state
│ ├── state.js # Centralized application state management object (appState)
│ ├── sync.js # Core animation loop and playback synchronization logic
│ ├── theme.js # Dark/Light mode theme switching logic
│ ├── ui.js # NEW: General UI event listeners (menus, modals, toggles)
│ ├── utils.js # General utility functions (binary search, formatting)
│ └── p5/
│ ├── radarSketch.js # p5.js sketch for the main radar visualization
│ ├── speedGraphSketch.js # p5.js sketch for the speed graph
│ └── zoomSketch.js # p5.js sketch for the magnified zoom window ("GOD MODE")
├── vendor/ # Local copies of external libraries
│ ├── ag-grid-community.min.js
│ ├── chart.min.js
│ ├── clarinet.min.js
│ ├── oboe.min.js
│ ├── p5.js
│ └── tailwind-cdn.js
├── tests/ # Unit tests
│ ├── fileLoader.test.js
│ ├── fileParsers.test.js
│ ├── test-runner.html
│ └── utils.test.js
├── Data_structs/ # Documentation of JSON and ROS2 data structures
└── Console_logs/ # Example logs for debugging synchronization
```
---
## 🚀 How to Use
1. **Load Files**: Use "Load JSON"/"Load Video" buttons or **Drag & Drop**.
2. **Playback**: Control via Spacebar, timeline slider (drag/scroll), or Arrow keys.
3. **Inspect**: Click the Radar canvas or press `I` to open the **Data Explorer**.
4. **Workspace**: Reorganize panels. Your layout is automatically saved to local storage.
5. **Soft-Reset**: Use the "Clear Cache and Reload" button to perform a complete factory reset.
---
## 🛠️ Technical Setup
The project is designed to run as a static application but requires a local server for Web Workers and File API access.
- **Check Environment**: Run `python_check.bat`.
- **Start Server**: Run `Visualization_Start.bat`.
- **Access**: Open `http://localhost:8000` in any modern browser.

6
steps/package-lock.json

@ -0,0 +1,6 @@
{
"name": "steps",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

230
steps/readme.md

@ -1,230 +0,0 @@
Radar and Video Synchronizer (Refactored)
Version: Based on commit f426eee3 (Inferred)
Overview
This is a high-precision, browser-based tool for visualizing radar point cloud data, object tracks, and CAN bus speed data, synchronized with a corresponding video file. Originally a monolithic HTML application (V14_inliner_Stationary.html), it has been refactored into a modern, modular JavaScript application using ES6 Modules. This structure enhances maintainability, performance (especially with large datasets), and extensibility.
The application leverages p5.js for rendering visualizations, a Web Worker with the Clarinet.js streaming parser for efficient background JSON processing, IndexedDB for persistent file caching, Tailwind CSS for styling, and introduces a Data Explorer panel using AG Grid and Chart.js for in-depth data inspection.
Core Features Detailed
Synchronized Playback (sync.js):
Utilizes performance.now() for a high-resolution master clock, independent of potentially imprecise video events.
Calculates the target media time based on playback speed (speedSlider.value) and elapsed real time.
Maps the target media time (adjusted by the user-defined offsetInput.value) to the corresponding radar frame timestamp (timestampMs).
Uses a binary search (utils.js::findRadarFrameIndexForTime) to efficiently find the correct radar frame index for the calculated timestamp.
Periodically checks for drift (>150ms) between the master clock's calculated time and videoPlayer.currentTime, forcing a video seek if needed to maintain sync.
Unified File Loading (main.js):
Handles loading JSON (radar data) and Video files through both button clicks (loadJsonBtn, loadVideoBtn) and drag-and-drop onto the main content area (<main>).
The handleFiles function identifies file types (.json, video/*) and triggers the processFilePipeline.
Note: Dedicated CAN log loading (loadCanBtn, canFileInput) has been removed. CAN speed data (canVehSpeed_kmph) is now expected within the JSON structure, associated with each radarFrame.
Efficient JSON Parsing (parser.worker.js, main.js, fileParsers.js):
The processFilePipeline function initiates a Web Worker (parser.worker.js).
The worker receives the JSON File object.
It uses the File.stream() API and TextDecoder to read the file chunk by chunk.
Clarinet.js (clarinet.min.js via CDN import in worker) parses the incoming JSON stream event-by-event (onopenobject, onkey, onvalue, etc.), reconstructing the full JavaScript object in memory off the main thread.
Progress updates (percent) are sent back to the main thread via postMessage.
On completion, the fully parsed object (data) is sent back.
The main thread receives the parsed data and passes it to fileParsers.js::parseVisualizationJson for post-processing (calculating relative timestampMs, determining global SNR range).
Interactive Visualization (p5/, drawUtils.js):
radarSketch.js: The primary p5 sketch.
Manages the main canvas within #canvas-container.
Calculates plot scales (plotScaleX, plotScaleY) based on canvas size and constants.js boundaries.
Draws axes, ego vehicle representation (drawUtils.js::drawEgoVehicle).
Draws dynamic elements: point clouds (drawUtils.js::drawPointCloud), tracks (drawUtils.js::drawTrajectories), track markers (drawUtils.js::drawTrackMarkers), predicted positions (now drawn for the current frame currentFrame, not currentFrame + 1), covariance ellipses (drawUtils.js::drawCovarianceEllipse), cluster centroids (drawUtils.js::drawClusterCentroids), and regions of interest (drawUtils.js::drawRegionsOfInterest).
Handles hover interactions for the Zoom Mode ("GOD MODE") tooltip via drawUtils.js::handleCloseUpDisplay.
Draws legends (SNR, Track TTC).
speedGraphSketch.js: The secondary p5 sketch for the speed graph.
Manages the canvas within #speed-graph-container.
Draws time-series lines for Ego Speed (frame.egoVelocity[1] * 3.6) and CAN Speed (frame.canVehSpeed_kmph) from the JSON vizData.
Draws axes and legends.
Draws a vertical red line indicator synchronized with the current frame's timestamp (frameData.timestampMs).
zoomSketch.js: p5 sketch for the magnified "GOD MODE" view.
Manages a separate canvas within #zoom-canvas-container.
Receives mouse coordinates and hovered items from radarSketch.js.
Redraws a scaled and translated portion of the main scene, providing a high-fidelity zoom.
Includes detailed tooltip drawing logic (drawZoomTooltip) showing extensive info for points, clusters, tracks (with speed), and predictions (with velocity).
Handles mouse wheel events for adjusting appState.zoomFactor.
drawUtils.js: Contains numerous helper functions responsible for the actual drawing logic (shapes, lines, colors, text) used by radarSketch.js and zoomSketch.js. Defines color palettes (snrColors, ttcColors, clusterColors). Refined tooltip data generation in handleCloseUpDisplay.
Data Explorer (dataExplorer.js, main.js, index.html):
A dedicated panel (#data-explorer-panel) for inspecting raw data of the current frame (appState.currentFrame).
Activated by pressing the <kbd>I</kbd> key or clicking on the main radar canvas (#canvas-container).
Features three views selectable via tabs:
Tree View (#tab-panel-tree): Displays the current frame's data structure as a formatted JSON string.
Grid View (#tab-panel-grid): Uses AG Grid (ag-grid-community.min.js) to display array data (e.g., pointCloud) in a sortable, filterable table. Populated via displayInGrid(data, title).
Plot View (#tab-panel-plot): Uses Chart.js (chart.js CDN) to generate a line plot of the numeric data from a column selected in the Grid View (by clicking the column header).
Allows users to directly examine the underlying numerical values being visualized.
Dynamic Filtering & Coloring (dom.js, drawUtils.js, state.js):
Checkboxes in the sidebar (#collapsible-menu) control boolean flags in appState (via main.js event listeners).
drawUtils.js functions read these flags (toggleSnrColor.checked, toggleConfirmedOnly.checked, etc.) to determine:
Which color palette/logic to apply to points and tracks.
Whether to show certain elements (tracks, velocity vectors, predicted positions, covariance).
SNR range inputs (snrMinInput, snrMaxInput) update appState.globalMinSnr/MaxSnr for color scaling.
TTC coloring mode (ttcModeDefault, ttcModeCustom) and custom inputs (ttcColorCritical, ttcTimeCritical, etc.) update appState.useCustomTtcScheme and appState.customTtcScheme respectively (dom.js event listeners). drawUtils.js::drawTrajectories uses this state to color tracks.
Playback Controls & Navigation (main.js, dom.js):
Standard buttons (playPauseBtn, stopBtn) modify appState.isPlaying and call videoPlayer.play/pause/currentTime.
timelineSlider input event updates appState.currentFrame and calls dom.js::updateFrame(frame, true) (forcing video seek). Throttled for performance during drag, debounced for final sync on release.
timelineSlider wheel event calculates scroll speed and dynamically seeks frames, also debounced for final sync (main.js).
timelineSlider mousemove event calculates hover position to display frame/time in #timeline-tooltip (main.js).
speedSlider updates videoPlayer.playbackRate (main.js).
UI Enhancements (main.js, theme.js, dom.js, index.html):
Sidebar (#collapsible-menu) visibility toggled by toggleMenuBtn, closeMenuBtn, and clicks on the #menu-scrim overlay (main.js::toggleMenu).
Fullscreen toggled via fullscreenBtn and monitored using the fullscreenchange event (main.js).
Persistent overlays (#radar-info-overlay, #video-info-overlay) updated by dom.js::updatePersistentOverlays with frame index, absolute time, color mode, and sync drift.
Dark/Light theme managed by theme.js::setTheme, saving preference to localStorage, and triggering redraws in p5 sketches.
Session Management (main.js, db.js):
Files are cached in IndexedDB using db.js::saveFileWithMetadata upon loading. Metadata (filename, size) is stored alongside the blob.
On DOMContentLoaded, main.js retrieves expected filenames from localStorage and attempts to load corresponding blobs using db.js::loadFreshFileFromDB. This function verifies filename and size match before returning the blob, ensuring cache validity.
saveSessionBtn gathers current state (appState filenames, offsetInput.value, toggle states) into a JSON object and triggers a browser download (main.js).
loadSessionBtn reads a chosen session JSON file. It verifies that the files mentioned in the session currently exist and are valid in IndexedDB using loadFreshFileFromDB before applying settings to localStorage and reloading the page (main.js).
Keyboard Shortcuts (main.js):
A comprehensive keydown listener intercepts keys (Space, Arrows, 1-4, S, T, D, G, P, A, M, Q, R, C, I).
It programmatically triggers .click() events on corresponding buttons or directly updates appState/calls functions (like showExplorer/hideExplorer).
Includes a check to prevent shortcuts firing when focus is on text/number inputs (offsetInput, snrMinInput, etc.).
How to Run Locally
(Instructions remain the same - requires Python 3 and running python -m http.server 8000 or the provided .bat script in the steps directory).
Check Python Installation: Run python_check.bat or python --version. Need Python 3.x in PATH.
Navigate to Project Directory: cd to the steps directory containing index.html.
Start Local Server: Run Visualization_Start.bat or python -m http.server 8000. Keep terminal open.
Open in Browser: Navigate to http://localhost:8000.
Project Structure
├── index.html # Main HTML shell
├── README.md # This documentation
├── Visualization_Start.bat # Script to start the local server
├── python_check.bat # Script to check Python installation
├── Visualizer_quick_start_Guide.html # Separate HTML quick start guide
├── favicon.png # Browser tab icon
└── src/
├── constants.js # Shared constants (radar bounds, FPS)
├── dataExplorer.js # NEW: Logic for the Data Explorer panel
├── db.js # IndexedDB logic for caching files
├── dom.js # DOM element references and UI update functions
├── drawUtils.js # p5.js drawing helpers (points, tracks, axes, legends)
├── fileParsers.js # Post-processing logic for parsed JSON
├── main.js # Main application entry point, event wiring, initialization
├── modal.js # Logic for pop-up modal dialogs & progress bar
├── parser.worker.js # Web Worker for background JSON parsing (uses Clarinet.js)
├── state.js # Centralized application state management object (appState)
├── sync.js # Core animation loop and playback synchronization logic
├── theme.js # Dark/Light mode theme switching logic
├── utils.js # General utility functions (binary search, timestamp parsing, formatting)
└── p5/
├── radarSketch.js # p5.js sketch for the main radar visualization
├── speedGraphSketch.js # p5.js sketch for the speed graph
└── zoomSketch.js # p5.js sketch for the magnified zoom window ("GOD MODE")
├── tests/ # Simple unit tests (optional)
│ ├── test-runner.html
│ ├── utils.test.js
│ └── fileParsers.test.js
└── context.md # Detailed technical overview for AI assistance
How to Use the Application
(Usage instructions remain largely similar, with additions for the Data Explorer)
Load Files: Use "Load JSON"/"Load Video" buttons or Drag & Drop. Offset calculated automatically from filenames (fHist_...json, WIN_...mp4).
Playback: Use UI buttons (<kbd>Space</kbd>), timeline slider (drag, hover, scroll wheel), or arrow keys (←/→).
Adjust Offset: Manually enter offset (ms, +ve if radar lags) and press Enter.
Adjust Speed: Use the "Speed" slider.
Use Sidebar (<kbd>M</kbd>): Access toggles (Color modes <kbd>1-4</kbd>/<kbd>S</kbd>, Tracks <kbd>T</kbd>, Details <kbd>D</kbd>, Zoom <kbd>G</kbd>, Predicted Pos <kbd>P</kbd>, Debug <kbd>A</kbd>, Raw Only <kbd>C</kbd>, Confirmed Only), SNR range, TTC customization.
Data Explorer (<kbd>I</kbd> / Canvas Click):
Press <kbd>I</kbd> or click the main radar canvas to open/close the panel.
View current frame data structure in the Tree View.
If applicable data is sent (e.g., pointCloud via canvas click), view it in the Grid View. Sort/filter columns.
In Grid View, click a column header, then click "Plot Selected Column" to see a line graph in the Plot View.
Session Management: Use "Save/Load Session", "Clear Cache". Load requires files in cache.
Other Shortcuts: Theme <kbd>Q</kbd>, Reset <kbd>R</kbd>.

31
steps/server.py

@ -0,0 +1,31 @@
import http.server
import socketserver
import os
import sys
# Ensure the server runs from the same directory as the script
os.chdir(os.path.dirname(os.path.abspath(__file__)))
PORT = 8000
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
# Disable caching for all files to ensure latest assets are loaded
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
super().end_headers()
Handler = MyHTTPRequestHandler
print(f"Starting server on http://127.0.0.1:{PORT}")
print("Working directory:", os.getcwd())
print("Cache-busting enabled: Browser will always fetch latest files.")
# Explicitly bind to 127.0.0.1 for better reliability in offline/local environments
with socketserver.TCPServer(("127.0.0.1", PORT), Handler) as httpd:
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped.")
httpd.server_close()

171
steps/src/dataExplorer.js

@ -2,6 +2,7 @@
import { appState } from './state.js';
import { throttle } from './utils.js';
import { makeDraggableAndResizable } from './ui.js';
import {
canvasContainer,
explorerBtn,
@ -18,11 +19,13 @@ const tabs = {
tree: { btn: document.getElementById('tab-btn-tree'), panel: document.getElementById('tab-panel-tree') },
grid: { btn: document.getElementById('tab-btn-grid'), panel: document.getElementById('tab-panel-grid') },
trackGrid: { btn: document.getElementById('tab-btn-track-grid'), panel: document.getElementById('tab-panel-track-grid') },
adas: { btn: document.getElementById('tab-btn-adas'), panel: document.getElementById('tab-panel-adas') },
plot: { btn: document.getElementById('tab-btn-plot'), panel: document.getElementById('tab-panel-plot') },
};
const gridDiv = document.getElementById('data-grid');
const trackGridDiv = document.getElementById('track-data-grid');
const adasContainer = document.getElementById('adas-vertical-view');
const chartCanvas = document.getElementById('data-chart');
// --- Module-Local State ---
@ -31,6 +34,9 @@ let trackGridApi = null;
let chartInstance = null;
let currentGridData = null;
// --- EXPORTED STATE for Optimization ---
export let isExplorerOpen = false;
// --- AG Grid Configuration ---
const gridOptions = {
rowData: [],
@ -102,11 +108,13 @@ function createChart(data, label) {
function showExplorer() {
panel.classList.remove('hidden');
isExplorerOpen = true; // Update state
updateExplorer();
}
function hideExplorer() {
panel.classList.add('hidden');
isExplorerOpen = false; // Update state
}
function switchTab(targetTab) {
@ -164,6 +172,7 @@ function updateExplorer() {
displayInGrid(frame.pointCloud, `${appState.currentFrame + 1}`);
// --- END: Auto-update Point Cloud Grid ---
displayTracksInGrid(tracksForCurrentFrame);
displayAdasData(frame.adas);
}
@ -243,6 +252,74 @@ function displayTracksInGrid(trackData) {
trackGridApi.setGridOption('rowData', trackData);
}
/**
* Renders ADAS data as a vertical property list (cards).
* Rationale: ADAS objects have many properties but few entries per frame.
* A vertical layout is much more readable than a wide horizontal grid.
*/
function displayAdasData(adasData) {
if (!adasContainer) return;
adasContainer.innerHTML = '';
if (!Array.isArray(adasData) || adasData.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.className = 'text-gray-500 text-center p-4';
emptyMsg.textContent = 'No ADAS data for this frame';
adasContainer.appendChild(emptyMsg);
return;
}
tabs.adas.btn.textContent = `ADAS Data: Frame ${appState.currentFrame + 1}`;
adasData.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 overflow-hidden';
const table = document.createElement('div');
table.className = 'grid grid-cols-[1fr_auto] gap-px bg-gray-200 dark:bg-gray-600';
// Add Table Headers
const keyHeader = document.createElement('div');
keyHeader.className = 'bg-gray-100 dark:bg-gray-700 px-2 py-1 text-[10px] font-bold uppercase text-gray-400 border-b border-gray-200 dark:border-gray-600';
keyHeader.textContent = 'Key';
const valHeader = document.createElement('div');
valHeader.className = 'bg-gray-100 dark:bg-gray-700 px-2 py-1 text-[10px] font-bold uppercase text-gray-400 border-b border-gray-200 dark:border-gray-600 text-right min-w-[80px]';
valHeader.textContent = 'Value';
table.appendChild(keyHeader);
table.appendChild(valHeader);
Object.entries(item).forEach(([key, value]) => {
const keyEl = document.createElement('div');
keyEl.className = 'bg-white dark:bg-gray-800 px-2 py-1 text-[11px] font-medium text-gray-500 dark:text-gray-400 truncate';
keyEl.textContent = key;
const valEl = document.createElement('div');
valEl.className = 'bg-white dark:bg-gray-800 px-2 py-1 text-[11px] font-mono text-gray-900 dark:text-gray-100 text-right';
// Apply formatting
if (typeof value === 'number') {
if (Number.isInteger(value)) {
valEl.textContent = value;
} else if (key === 'snr') {
valEl.textContent = value.toFixed(2);
} else {
valEl.textContent = value.toFixed(4);
}
} else if (Array.isArray(value)) {
valEl.textContent = `[${value.map(v => typeof v === 'number' ? v.toFixed(3) : v).join(', ')}]`;
} else {
valEl.textContent = value;
}
table.appendChild(keyEl);
table.appendChild(valEl);
});
card.appendChild(table);
adasContainer.appendChild(card);
});
}
// --- START: New Robust Update Logic ---
let throttleTimer = null;
@ -263,97 +340,6 @@ export function throttledUpdateExplorer() {
}
// --- END: New Robust Update Logic ---
// --- START: Resizable and Draggable Panel Logic ---
function makePanelInteractive(panel) {
const header = document.getElementById('data-explorer-header');
const resizers = panel.querySelectorAll('.resizer');
const minWidth = 400;
const minHeight = 300;
let original_width = 0;
let original_height = 0;
let original_x = 0;
let original_y = 0;
let original_mouse_x = 0;
let original_mouse_y = 0;
// --- Dragging Logic ---
header.addEventListener('mousedown', (e) => {
e.preventDefault();
original_x = panel.offsetLeft;
original_y = panel.offsetTop;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
document.body.classList.add('dragging');
window.addEventListener('mousemove', dragPanel);
window.addEventListener('mouseup', stopDrag);
});
function dragPanel(e) {
const dx = e.pageX - original_mouse_x;
const dy = e.pageY - original_mouse_y;
panel.style.left = `${original_x + dx}px`;
panel.style.top = `${original_y + dy}px`;
}
function stopDrag() {
document.body.classList.remove('dragging');
window.removeEventListener('mousemove', dragPanel);
window.removeEventListener('mouseup', stopDrag);
}
// --- Resizing Logic ---
resizers.forEach(resizer => {
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
original_width = parseFloat(getComputedStyle(panel, null).getPropertyValue('width').replace('px', ''));
original_height = parseFloat(getComputedStyle(panel, null).getPropertyValue('height').replace('px', ''));
original_x = panel.getBoundingClientRect().left;
original_y = panel.getBoundingClientRect().top;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
const resizeFunc = (event) => resizePanel(event, resizer.classList);
document.body.classList.add('resizing');
window.addEventListener('mousemove', resizeFunc);
window.addEventListener('mouseup', () => {
document.body.classList.remove('resizing');
window.removeEventListener('mousemove', resizeFunc);
});
});
});
function resizePanel(e, direction) {
// --- START: Fix for Resizing Logic ---
// The logic is updated to handle corners correctly by checking for 't', 'b', 'l', 'r' substrings.
// This allows a corner like 'resizer-br' to trigger both bottom and right resizing logic.
if (direction.toString().includes('r')) { // Check for right edge
const width = original_width + (e.pageX - original_mouse_x);
if (width > minWidth) panel.style.width = `${width}px`;
}
if (direction.toString().includes('b')) { // Check for bottom edge
const height = original_height + (e.pageY - original_mouse_y);
if (height > minHeight) panel.style.height = `${height}px`;
}
if (direction.toString().includes('l')) { // Check for left edge
const newWidth = original_width - (e.pageX - original_mouse_x);
if (newWidth > minWidth) {
panel.style.width = `${newWidth}px`;
panel.style.left = `${original_x + (e.pageX - original_mouse_x)}px`;
}
}
if (direction.toString().includes('t')) { // Check for top edge
const newHeight = original_height - (e.pageY - original_mouse_y);
if (newHeight > minHeight) {
panel.style.height = `${newHeight}px`;
panel.style.top = `${original_y + (e.pageY - original_mouse_y)}px`;
}
}
// --- END: Fix for Resizing Logic ---
}
}
function initializePanelPosition(panel) {
// Remove Tailwind classes that conflict with dynamic positioning/sizing
panel.classList.remove('bottom-24', 'right-4', 'w-full', 'max-w-2xl', 'h-1/2');
@ -383,7 +369,8 @@ export function initializeDataExplorer() {
// --- START: Make panel interactive ---
initializePanelPosition(panel);
makePanelInteractive(panel);
// Rationale for 250x200: Allows for a very compact "sidecar" view when using the ADAS Property View.
makeDraggableAndResizable(panel, document.getElementById('data-explorer-header'), 250, 200);
// --- END: Make panel interactive ---
// --- Wire up all event listeners ---

148
steps/src/db.js

@ -1,80 +1,177 @@
// In src/db.js, replace the entire file content with this:
let db;
let dbReadyPromise;
let dbReadyResolve;
import { showModal } from "./modal.js";
// Initialize the promise that tracks DB readiness
dbReadyPromise = new Promise((resolve) => {
dbReadyResolve = resolve;
});
// Initializes the IndexedDB database.
// Opens or creates the 'visualizerDB' database.
export function initDB(callback) {
const request = indexedDB.open("visualizerDB", 1);
export function initDB() {
const request = indexedDB.open("visualizerDB", 2);
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains("files")) {
db.createObjectStore("files");
// Creates an object store named 'files' if it doesn't exist.
}
// Create the new store for manual offsets
if (!db.objectStoreNames.contains("manualOffsets")) {
db.createObjectStore("manualOffsets");
}
};
request.onsuccess = function (event) {
db = event.target.result;
console.log("Database initialized");
// Assigns the opened database to the 'db' variable.
if (callback) callback();
dbReadyResolve(db); // Signal that DB is ready
};
request.onerror = function (event) {
console.error("IndexedDB error:", event.target.errorCode);
// Even if DB fails, call the callback so the app doesn't hang
// Logs any errors during database operations.
// Calls the callback even if there's an error to prevent the app from hanging.
if (callback) callback();
// If DB fails, we resolve with null so operations can proceed (gracefully failing to cache)
dbReadyResolve(null);
};
return dbReadyPromise;
}
// Ensure DB is ready before returning it
async function getDB() {
if (db) return db;
return await dbReadyPromise;
}
// Saves a file (Blob) along with its metadata into the IndexedDB.
export function saveFileWithMetadata(key, file) {
if (!db) return;
return new Promise(async (resolve, reject) => {
const database = await getDB();
const transaction = db.transaction(["files"], "readwrite");
if (!database) {
// If DB failed to initialize, just warn and skip caching
console.warn("Database not available. Skipping cache save.");
resolve();
return;
}
const transaction = database.transaction(["files"], "readwrite");
const store = transaction.objectStore("files");
// Creates a read-write transaction and gets the 'files' object store.
// Store an object containing the blob and its metadata
const dataToStore = {
filename: file.name,
size: file.size,
type: file.type,
blob: file
// Prepares the data object to be stored, including filename, size, type, and the file itself (as a Blob).
};
const request = store.put(dataToStore, key);
request.onsuccess = () => console.log(`File '${file.name}' saved to DB with metadata.`);
request.onsuccess = () => {
console.log(`File '${file.name}' saved to DB with metadata.`);
resolve();
};
// Gracefully handle errors, especially quota limits
transaction.onerror = (event) => {
if (event.target.error.name === 'QuotaExceededError') {
alert("Could not cache file: Browser storage quota exceeded. The app will still work for this session.");
showModal("Could not cache file: Browser storage quota exceeded. The app will still work for this session, but files won't be saved for next time.");
resolve(); // Resolve anyway to let the app continue without caching
} else {
// Handles potential errors during the save operation, such as QuotaExceededError.
console.error(`Error saving file '${key}':`, event.target.error);
reject(event.target.error);
}
};
});
}
// Saves a manual offset for a specific filename.
export function saveManualOffset(filename, offset) {
return new Promise(async (resolve, reject) => {
const database = await getDB();
if (!database || !filename) {
resolve(); // Fail silently if DB is not available or filename is missing
return;
}
const transaction = database.transaction(["manualOffsets"], "readwrite");
const store = transaction.objectStore("manualOffsets");
const request = store.put(offset, filename); // Key is filename, value is offset
request.onsuccess = () => {
console.log(`Manual offset ${offset}ms saved for '${filename}'.`);
resolve();
};
request.onerror = (e) => {
console.warn("Failed to save manual offset:", e);
resolve(); // Resolve anyway to prevent blocking
};
});
}
// Loads a manual offset for a specific filename.
export function loadManualOffset(filename) {
return new Promise(async (resolve) => {
const database = await getDB();
if (!database || !filename) {
resolve(null);
return;
}
const transaction = database.transaction(["manualOffsets"], "readonly");
const store = transaction.objectStore("manualOffsets");
const request = store.get(filename);
request.onsuccess = () => {
const result = request.result;
if (result !== undefined) {
console.log(`Found saved manual offset for '${filename}': ${result}ms`);
resolve(result);
} else {
resolve(null);
}
};
request.onerror = () => {
resolve(null);
};
});
}
// Deletes a manual offset for a specific filename.
export function deleteManualOffset(filename) {
return new Promise(async (resolve) => {
const database = await getDB();
if (!database || !filename) {
resolve();
return;
}
const transaction = database.transaction(["manualOffsets"], "readwrite");
const store = transaction.objectStore("manualOffsets");
const request = store.delete(filename);
request.onsuccess = () => {
console.log(`Manual offset for '${filename}' deleted.`);
resolve();
};
request.onerror = (e) => {
console.warn("Failed to delete manual offset:", e);
resolve();
};
});
}
// Loads a file from IndexedDB, performing checks for filename and size to ensure data integrity.
export function loadFreshFileFromDB(key, expectedFilename) {
return new Promise((resolve) => {
if (!db || !expectedFilename) {
return new Promise(async (resolve) => {
const database = await getDB();
if (!database || !expectedFilename) {
resolve(null);
return;
}
const transaction = db.transaction(["files"], "readonly");
// Creates a read-only transaction.
const transaction = database.transaction(["files"], "readonly");
const store = transaction.objectStore("files");
const request = store.get(key);
@ -82,21 +179,18 @@ export function loadFreshFileFromDB(key, expectedFilename) {
const cachedData = request.result;
if (!cachedData) {
console.log(`Cache miss for key '${key}': No data found.`);
// If no data is found for the key, resolve with null.
resolve(null);
return;
}
// 1. Versioning Check: Do the filenames match?
if (cachedData.filename !== expectedFilename) {
// Checks if the cached filename matches the expected filename.
console.warn(`Cache miss for key '${key}': Stale data found (Filename mismatch).`);
resolve(null);
return;
}
// 2. Integrity Check: Do the sizes match?
// Checks if the cached file size matches the stored size metadata.
if (cachedData.blob.size !== cachedData.size) {
console.error(`Cache miss for key '${key}': Corrupted data found (Size mismatch).`);
resolve(null);
@ -104,14 +198,12 @@ export function loadFreshFileFromDB(key, expectedFilename) {
}
// All checks passed!
// If all checks pass, resolve with the cached Blob.
console.log(`Cache hit for '${expectedFilename}'`);
resolve(cachedData.blob);
};
request.onerror = (event) => {
console.error(`Error loading file '${key}' from DB:`, event.target.error);
// Logs any errors during the load operation.
resolve(null);
};
});

24
steps/src/debug.js

@ -0,0 +1,24 @@
// This file centralizes all debug logging flags for the application.
// To enable a specific set of logs, set the corresponding flag to `true`.
// These flags can also be modified at runtime via the browser console
// by accessing the global `debugFlags` object (e.g., `debugFlags.sync = true`).
export const debugFlags = {
// Logs from videoFrameCallback and animationLoop in sync.js
sync: false,
// Logs from the main p5.js draw() functions (e.g., radarSketch.js)
drawing: false,
// Logs related to file loading, parsing, and caching
fileLoading: false,
// Logs from the SpeedGraph p5 sketch (density info, etc.)
speedGraph: false,
// If true, file caching blocks the main thread for debugging.
CACHE_BLOCKING: false,
VIDEO_LOAD_TIMEOUT: 10000, // 10 seconds
VIDEO_LOAD_RETRIES: 1, // Number of retries if loading fails
};

379
steps/src/dom.js

@ -2,8 +2,44 @@
import { appState } from "./state.js";
import { formatUTCTime } from "./utils.js";
import { VIDEO_FPS } from "./constants.js";
function getTimingColor(diffMs) {
if (diffMs >= 48 && diffMs <= 52) {
return "#00FF00"; // Bright Green (Perfect)
} else if (diffMs >= 40 && diffMs <= 60) {
return "#98FB98"; // Pale Green (Good)
} else if ((diffMs >= 30 && diffMs < 40) || (diffMs > 60 && diffMs <= 70)) {
return "#FFEB3B"; // Yellow (Noticeable)
} else if ((diffMs >= 20 && diffMs < 30) || (diffMs > 70 && diffMs <= 100)) {
return "#FFA500"; // Orange (Warning)
} else if (diffMs > 300) {
return "#c339ffff"; // Dark Violet (Extreme > 300ms)
} else if (diffMs > 150) {
return "#8B0000"; // Dark Red (Severe > 150ms)
} else {
return "#FF4500"; // Red (Critical 100-150ms or < 20ms)
}
}
// --- DOM Element References --- //
export const startScreenModal = document.getElementById("start-screen-modal");
export const startDropZone = document.getElementById("start-drop-zone");
export const startLoadJsonBtn = document.getElementById("start-load-json-btn");
export const startLoadVideoBtn = document.getElementById("start-load-video-btn");
export const startClearCacheBtn = document.getElementById("start-clear-cache-btn");
export const startProgressContainer = document.getElementById("start-progress-container");
export const startProgressBar = document.getElementById("start-progress-bar");
export const startProgressText = document.getElementById("start-progress-text");
export const startUserManualBtn = document.getElementById("start-user-manual-btn");
export const startCodebaseBtn = document.getElementById("start-codebase-btn");
export const startChangelogBtn = document.getElementById("start-changelog-btn");
export const startThemeToggleBtn = document.getElementById("start-theme-toggle");
export const startThemeToggleDarkIcon = document.getElementById("start-theme-toggle-dark-icon");
export const startThemeToggleLightIcon = document.getElementById("start-theme-toggle-light-icon");
export const globalDragOverlay = document.getElementById("global-drag-overlay");
export const themeToggleBtn = document.getElementById("theme-toggle");
export const canvasContainer = document.getElementById("canvas-container");
export const canvasPlaceholder = document.getElementById("canvas-placeholder");
@ -52,6 +88,7 @@ export const modalCancelBtn = document.getElementById("modal-cancel-btn");
export const toggleCloseUp = document.getElementById("toggle-close-up");
export const togglePredictedPos = document.getElementById("toggle-predicted-pos");
export const toggleCovariance = document.getElementById("toggle-covariance");
export const toggleVehicleDimensions = document.getElementById("toggle-vehicle-dimensions");
export const modalProgressContainer = document.getElementById("modal-progress-container");
export const modalProgressBar = document.getElementById("modal-progress-bar");
export const modalProgressText = document.getElementById("modal-progress-text");
@ -81,25 +118,61 @@ export const fullscreenExitIcon = document.getElementById("fullscreen-exit-icon"
export const menuScrim = document.getElementById("menu-scrim");
export const toggleConfirmedOnly = document.getElementById("toggle-confirmed-only");
export const explorerBtn = document.getElementById("explorer-btn");
export const shortcutsBtn = document.getElementById("shortcuts-btn");
export const shortcutsModal = document.getElementById("shortcuts-modal");
export const shortcutsModalCloseBtn = document.getElementById("shortcuts-modal-close-btn");
export const userManualBtn = document.getElementById("user-manual-btn");
export const guideModal = document.getElementById("guide-modal");
export const guideModalCloseBtn = document.getElementById("guide-modal-close-btn");
export const codebaseBtn = document.getElementById("codebase-btn");
export const codebaseModal = document.getElementById("codebase-modal");
export const codebaseModalCloseBtn = document.getElementById("codebase-modal-close-btn");
export const changelogBtn = document.getElementById("changelog-btn");
export const changelogModal = document.getElementById("changelog-modal");
export const changelogModalCloseBtn = document.getElementById("changelog-modal-close-btn");
export const rangeSlider = document.getElementById("range-slider");
export const rangeValueDisplay = document.getElementById("range-value-display");
//----------------------Reset UI for New file Load----------------------//
// Resets the UI to make sure everything is clean before new files load.
export function resetUIForNewLoad() {
console.log("Resetting UI for new file load.");
// @param {boolean} isNewVideo - If true, the video player will be reset. If false, existing video is preserved.
export function resetUIForNewLoad(isNewVideo = true) {
console.log(`Resetting UI for new file load. New Video: ${isNewVideo}`);
// Hide feature toggles
featureToggles.classList.add("hidden");
// Show placeholders
canvasPlaceholder.style.display = 'flex';
videoPlaceholder.classList.remove('hidden');
// Reset the FPS counter state to prevent incorrect calculations on reload
appState.fps = 0;
appState.lastOverlayUpdateTime = 0;
// Hide video player and overlays
// --- Conditional Video Reset ---
if (isNewVideo || !videoPlayer.src) {
// Reset video UI: Show placeholder, hide player, clear source
videoPlaceholder.classList.remove('hidden');
videoPlayer.classList.add('hidden');
videoPlayer.src = ''; // Clear the video source
radarInfoOverlay.classList.add('hidden');
videoInfoOverlay.classList.add('hidden');
} else {
// Preserve video UI: Ensure player is visible, placeholder hidden
videoPlaceholder.classList.add('hidden');
videoPlayer.classList.remove('hidden');
// Do NOT clear videoPlayer.src
videoInfoOverlay.classList.remove('hidden');
}
// Show canvas placeholder (will be hidden later if data loads)
canvasPlaceholder.style.display = 'flex';
// Always hide radar overlay initially
radarInfoOverlay.classList.add('hidden');
// Reset offset indicator state
autoOffsetIndicator.classList.add("hidden");
autoOffsetIndicator.textContent = "";
autoOffsetIndicator.className = "text-xs font-bold ml-2 hidden"; // Reset classes
// Remove the p5 sketches completely
if (appState.p5_instance) {
@ -145,10 +218,15 @@ export function updateDebugOverlay(currentMediaTime) {
// --- Logic for the original debug overlay ---
if (isDebug1Visible) {
content.push(`--- Basic Info ---`);
if (appState.videoStartDate) {
const videoAbsoluteTimeMs =
appState.videoStartDate.getTime() + currentMediaTime * 1000;
content.push(`Media Time (s): ${currentMediaTime.toFixed(3)}`);
const baseTimeMs = appState.videoStartDate ? appState.videoStartDate.getTime() : (appState.radarStartTimeMs || 0);
const videoAbsoluteTimeMs = baseTimeMs + currentMediaTime * 1000;
let timeString = `Media Time (s): ${currentMediaTime.toFixed(2)}`; // Two decimal places
if (videoPlayer && !isNaN(videoPlayer.duration) && videoPlayer.duration > 0) {
timeString += ` / ${videoPlayer.duration.toFixed(2)}`; // Add total duration with two decimal places
}
content.push(timeString);
content.push(`Video Frame: ${Math.floor(currentMediaTime * VIDEO_FPS)}`);
content.push(
`Vid Abs Time: ${new Date(videoAbsoluteTimeMs)
@ -156,20 +234,18 @@ export function updateDebugOverlay(currentMediaTime) {
.split("T")[1]
.replace("Z", "")}`
); // Format and display video absolute time
} else {
content.push("Video not loaded..."); // Indicate video not loaded.
}
if (
appState.vizData &&
appState.vizData.radarFrames[appState.currentFrame]
) {
content.push(`Radar Frame: ${appState.currentFrame + 1}`);
const frameTime =
appState.vizData.radarFrames[appState.currentFrame].timestampMs;
const frameData = appState.vizData.radarFrames[appState.currentFrame];
const frameTime = frameData.timestampMs;
const baseTimeMs = appState.videoStartDate ? appState.videoStartDate.getTime() : (appState.radarStartTimeMs || 0);
content.push(
`Radar Abs Time: ${new Date(
appState.videoStartDate.getTime() + frameTime
)
`Radar Abs Time: ${new Date(baseTimeMs + frameTime)
.toISOString()
.split("T")[1]
.replace("Z", "")}`
@ -181,28 +257,24 @@ export function updateDebugOverlay(currentMediaTime) {
if (isDebug2Visible) {
content.push(`--- Sync Diagnostics ---`);
if (
appState.videoStartDate &&
appState.vizData &&
appState.vizData.radarFrames[appState.currentFrame]
) {
// --- START: Corrected Debug Logic ---
const currentRadarFrame =
appState.vizData.radarFrames[appState.currentFrame];
const targetRadarTimeMs = currentRadarFrame.timestampMs;
const offsetMs = parseFloat(offsetInput.value) || 0; // Read the current offset
// Make the drift calculation "offset-aware"
const driftMs = currentMediaTime * 1000 + offsetMs - targetRadarTimeMs;
// --- END: Corrected Debug Logic ---
// The correct drift is the difference between the video's actual time and the pre-calculated "baked-in" sync time for the current radar frame.
const driftMs = (currentMediaTime - currentRadarFrame.videoSyncedTime) * 1000;
// Style the drift value to be green if sync is good, and red if it's off.
const driftColor = Math.abs(driftMs) > 40 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
const driftColor = Math.abs(driftMs) > 50 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
content.push(`Video Time (s): ${currentMediaTime.toFixed(3)}`); // Display current video time
content.push(`Target Radar Time (ms): ${targetRadarTimeMs.toFixed(0)}`);
content.push(`Target Sync Time (s): ${currentRadarFrame.videoSyncedTime.toFixed(3)}`);
content.push(`Drift (ms): <b style="color: ${driftColor};">${driftMs.toFixed(0)}</b>`);
content.push(`Video Start Time: ${appState.videoStartDate.toISOString()}`);
content.push(`Radar Start Time: ${new Date(appState.radarStartTimeMs).toISOString()}`);
const videoStart = appState.videoStartDate ? appState.videoStartDate.toISOString() : new Date(0).toISOString();
content.push(`Video Start Time: ${videoStart}`);
content.push(`Radar Start Time: ${new Date(appState.radarStartTimeMs || 0).toISOString()}`);
content.push(`Calculated Offset (ms): ${offsetInput.value}`); // Display calculated offset.
const renderTime = appState.lastFrameRenderTime;
// Color is green if render time is under 33ms (~30fps budget), otherwise red
@ -223,74 +295,259 @@ export function updateDebugOverlay(currentMediaTime) {
// This function checks the state of the color toggles and returns the active mode.
function getCurrentColorMode() {
if (toggleSnrColor.checked) return "Color by SNR (1)";
if (toggleClusterColor.checked) return "Color by Cluster (2)";
if (toggleClusterColor.checked) return "Cluster Mode (2)";
if (toggleInlierColor.checked) return "Color by Inlier (3)";
if (toggleStationaryColor.checked) return "Color by Stationary (4)";
return "Default"; // The default mode when no specific color toggle is checked
}
export function updatePersistentOverlays(currentMediaTime) {
// If we don't have the necessary data, hide the overlays and exit.
const isDebug1Visible = toggleDebugOverlay.checked;
const isDebug2Visible = toggleDebug2Overlay.checked;
// Cache for DOM elements to avoid querySelector/getElementById every frame
let overlayCache = null;
let videoOverlayCache = null;
if (!appState.vizData || !appState.videoStartDate) {
// Cache for conditional rendering
let lastDrawnFrame = -1;
let lastDrawnScale = -1;
export function updatePersistentOverlays(currentMediaTime) {
// If the advanced debug overlay is visible, hide the persistent overlays and exit.
if (toggleDebug2Overlay.checked) {
radarInfoOverlay.classList.add("hidden");
videoInfoOverlay.classList.add("hidden");
return;
}
if (isDebug1Visible && isDebug2Visible) {
// If we don't have the necessary data, hide the overlays and exit.
if (!appState.vizData) {
radarInfoOverlay.classList.add("hidden");
videoInfoOverlay.classList.add("hidden");
return;
}
if(isDebug1Visible || isDebug2Visible){
videoInfoOverlay.classList.add("hidden");
return;
}
// Otherwise, make sure they are visible.
radarInfoOverlay.classList.remove("hidden");
videoInfoOverlay.classList.remove("hidden");
// --- Update Radar Overlay ---
// --- Update Radar Persistent Overlay ---
const currentRadarFrame = appState.vizData.radarFrames[appState.currentFrame];
const frameData = appState.vizData.radarFrames[appState.currentFrame];
const motionState = frameData.motionState;
if (currentRadarFrame) {
const absRadarTime = new Date(
appState.videoStartDate.getTime() + currentRadarFrame.timestampMs
appState.radarStartTimeMs + currentRadarFrame.timestamp
);
const targetRadarTimeMs = currentRadarFrame.timestampMs;
const offsetMs = parseFloat(offsetInput.value) || 0;
const driftMs = currentMediaTime * 1000 + offsetMs - targetRadarTimeMs;
const driftMs = (currentMediaTime - currentRadarFrame.videoSyncedTime) * 1000;
const driftColor = Math.abs(driftMs) > 50 ? "#FF6347" : "#98FB98"; // Tomato or Pale Green
const colorMode = getCurrentColorMode();
const fps = appState.fps;
const fpsColor = fps >= 58 && fps <= 62 ? "#98FB98" : "#FF6347"; // Pale Green or Tomato
const interFrameTime = currentRadarFrame.interFrameTime;
const iftColor = getTimingColor(interFrameTime);
// --- OPTIMIZATION: One-time DOM Setup & Caching ---
if (!overlayCache) {
radarInfoOverlay.innerHTML = `
Frame: ${appState.currentFrame + 1}
Motion State: ${motionState}
| FPS: <b style="color: ${fpsColor};">${fps.toFixed(1)}</b>
| Abs Time: ${formatUTCTime(absRadarTime)}
| Color Mode: <b>${colorMode}</b>
| Drift: <b style="color: ${driftColor};">${driftMs.toFixed(
0
)}ms </b>
<div id="radar-text-content" style="line-height: 1.5; text-align: center;">
Frame: <span id="ov-frame"></span>
| EGO State: <span id="ov-motion"></span>
| FPS: <b id="ov-fps"></b>
| Color mode: <b id="ov-mode"></b>
| Drift: <b id="ov-drift"></b>
| Δt: <b id="ov-ift"></b>
<!-- | Scale: <b id="ov-scale"></b> -->
</div>
<canvas id="ift-dot-matrix" height="40" style="display:block; margin-top:5px; background:rgba(0,0,0,0.5); border:1px solid #555; width: 100%;"></canvas>
`;
overlayCache = {
frame: document.getElementById("ov-frame"),
motion: document.getElementById("ov-motion"),
fps: document.getElementById("ov-fps"),
mode: document.getElementById("ov-mode"),
drift: document.getElementById("ov-drift"),
ift: document.getElementById("ov-ift"),
// scale: document.getElementById("ov-scale"), // Commented out as requested
dotCanvas: document.getElementById("ift-dot-matrix") // Cache canvas too
};
}
// --- Update Video Overlay ---
const absVideoTime = new Date(
appState.videoStartDate.getTime() + currentMediaTime * 1000
);
// --- PERFORMANCE FIX: Read layout BEFORE writing to DOM ---
// Reading clientWidth here avoids "Forced Reflow" (Layout Thrashing) because
// we haven't dirtied the layout with text updates yet in this frame.
let currentCanvasWidth = 0;
if (overlayCache && overlayCache.dotCanvas) {
currentCanvasWidth = overlayCache.dotCanvas.clientWidth;
}
// --- 1. Smart Smooth Zoom Logic ---
// Use pre-calculated maxWindowIFT (computed in fileParsers.js) for O(1) performance.
const maxWindowIFT = currentRadarFrame.maxWindowIFT || 0;
// Calculate Target Scale
// Base: 10ms. If max > 100ms, scale up.
// Cap: 40ms (3-4x zoom).
let targetMsPerBlock = 10;
if (maxWindowIFT > 100) {
// Example: 900ms spike -> 900/10 = 90. Clamped to 40.
targetMsPerBlock = Math.min(40, Math.max(10, maxWindowIFT / 10));
}
// --- START: Frame-Rate Independent Smoothing ---
// We use performance.now() to calculate a delta time for smooth animations
// across different monitor refresh rates.
const now = performance.now();
const dt = appState.lastOverlayUpdateTime ? now - appState.lastOverlayUpdateTime : 16.67;
appState.lastOverlayUpdateTime = now;
// Smooth Interpolation (Lerp)
// Move current scale towards the target.
// If playing, use 0.1 (fast). If stopped, use 0.033 (slow, ~3x slower).
const baseSmoothing = appState.isPlaying ? 0.1 : 0.033;
const adjustedSmoothing = 1 - Math.pow(1 - baseSmoothing, dt / (1000 / 60));
appState.currentGraphScale += (targetMsPerBlock - appState.currentGraphScale) * adjustedSmoothing;
// --- END: Frame-Rate Independent Smoothing ---
// If the scale hasn't converged yet and we are NOT playing (main loop not running),
// request another frame to continue the smoothing animation.
if (!appState.isPlaying && Math.abs(targetMsPerBlock - appState.currentGraphScale) > 0.01) {
requestAnimationFrame(() => updatePersistentOverlays(videoPlayer.currentTime));
}
// Use the smoothed value for drawing
const msPerBlock = appState.currentGraphScale;
// --- Update Text Content Efficiently (Zero Garbage) ---
if (overlayCache) {
overlayCache.frame.textContent = appState.currentFrame + 1;
overlayCache.motion.textContent = motionState;
overlayCache.fps.textContent = fps.toFixed(1);
overlayCache.fps.style.color = fpsColor;
overlayCache.mode.textContent = colorMode;
overlayCache.drift.textContent = driftMs.toFixed(0) + "ms";
overlayCache.drift.style.color = driftColor;
overlayCache.ift.textContent = interFrameTime.toFixed(0) + "ms";
overlayCache.ift.style.color = iftColor;
// overlayCache.scale.textContent = "1:" + msPerBlock.toFixed(1) + "ms"; // Commented out as requested
}
// --- Draw Optimized Square Block Matrix Graph ---
// CONDITIONAL RENDER: Only redraw if frame changed or scale changed significantly
const dotCanvas = overlayCache.dotCanvas;
let isResized = false;
if (dotCanvas) {
if (dotCanvas.width !== currentCanvasWidth) {
dotCanvas.width = currentCanvasWidth;
isResized = true;
}
}
if (appState.currentFrame !== lastDrawnFrame || Math.abs(msPerBlock - lastDrawnScale) > 0.01 || isResized) {
if (dotCanvas) {
const ctx = dotCanvas.getContext("2d");
const w = dotCanvas.width;
const h = dotCanvas.height;
const blockSize = 3;
const vGap = 1;
const hGap = 2;
const stride = blockSize + hGap;
// msPerBlock is already set above
// Calculate columns dynamically based on width
const totalCols = Math.floor(w / stride);
const centerCol = Math.floor(totalCols / 2);
ctx.clearRect(0, 0, w, h);
// --- Optimization: Immediate Mode Drawing (No Allocations) ---
// Instead of batching into objects, we draw directly.
// We iterate through columns. To minimize state changes, we could pre-sort,
// but simply drawing column-by-column is fast enough and avoids GC.
for (let offset = -centerCol; offset < centerCol; offset++) {
const targetFrameIndex = appState.currentFrame + offset;
const colIndex = offset + centerCol;
const x = colIndex * stride + 2;
if (targetFrameIndex >= 0 && targetFrameIndex < appState.vizData.radarFrames.length) {
const ift = appState.vizData.radarFrames[targetFrameIndex].interFrameTime || 0;
// Use the SMOOTHED dynamic scale here
const numBlocks = Math.min(10, Math.max(1, Math.round(ift / msPerBlock)));
const color = getTimingColor(ift);
ctx.fillStyle = color;
ctx.beginPath();
for (let d = 0; d < numBlocks; d++) {
const y = h - (d * (blockSize + vGap)) - 3;
ctx.rect(x, y, blockSize, blockSize);
}
ctx.fill();
} else {
// Placeholder blocks
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
ctx.beginPath();
const y = h - 3;
ctx.rect(x, y, blockSize, blockSize);
ctx.fill();
}
}
// Draw Center Indicator (Triangle at center column)
const centerX = centerCol * stride + 2;
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
ctx.moveTo(centerX - 4, h);
ctx.lineTo(centerX + 4, h);
ctx.lineTo(centerX, h - 6);
ctx.fill();
}
// Update cache
lastDrawnFrame = appState.currentFrame;
lastDrawnScale = msPerBlock;
}
}
// --- Update Video Persistent Overlay ---
// Default to Unix Epoch (Jan 1, 1970) if no dates are available
const baseTimeMs = appState.videoStartDate ? appState.videoStartDate.getTime() : (appState.radarStartTimeMs || 0);
const absVideoTime = new Date(baseTimeMs + currentMediaTime * 1000);
const videoFrame = Math.floor(currentMediaTime * VIDEO_FPS);
//console.warn('Could not load radarframes ', appState.vizData.radarFrames) console warning for reference
let timeDisplay = `Elapsed Time: ${currentMediaTime.toFixed(2)}s`;
if (videoPlayer && !isNaN(videoPlayer.duration) && videoPlayer.duration > 0) {
timeDisplay += ` / ${videoPlayer.duration.toFixed(2)}s`;
}
// --- OPTIMIZATION: Video Overlay Caching ---
if (!videoOverlayCache) {
videoInfoOverlay.innerHTML = `
Frame: ${videoFrame}
| Abs Time: ${formatUTCTime(absVideoTime)}
Frame: <span id="ov-vid-frame"></span>
| <span id="ov-vid-time"></span>
| Abs Time: <span id="ov-vid-abs"></span>
`;
videoOverlayCache = {
frame: document.getElementById("ov-vid-frame"),
time: document.getElementById("ov-vid-time"),
abs: document.getElementById("ov-vid-abs")
};
}
if (videoOverlayCache) {
videoOverlayCache.frame.textContent = videoFrame;
videoOverlayCache.time.textContent = timeDisplay;
videoOverlayCache.abs.textContent = formatUTCTime(absVideoTime);
}
}
const customTtcInputs = [

453
steps/src/drawUtils.js

@ -1,11 +1,9 @@
import {
RADAR_X_MAX,
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
MAX_TRAJECTORY_LENGTH,
ROI_TRACKS_Y_MIN,
ROI_TRACKS_Y_MAX,
ROI_CLOSE_Y_MIN,
ROI_CLOSE_Y_MAX,
} from "./constants.js";
@ -72,48 +70,83 @@ export const clusterColors = (p) => [
export const stationaryColor = (p) => p.color(218, 165, 32); // Goldenrod
export const movingColor = (p) => p.color(255, 0, 255); // Magenta
export function getTrackRisk(track, log) {
if (log && log.risk !== undefined && log.risk !== null) {
return log.risk;
}
if (track && track.ttcCategoryTimeline && log) {
const entry = track.ttcCategoryTimeline.find((e) => e.frameIdx === log.frameIdx);
return entry ? entry.ttcCategory : null;
}
return null;
}
/**
* Draws the static radar region lines to a buffer.
* Draws the static radar region lines, axes, and ego vehicle to a buffer.
* @param {p5} p - The main p5 instance (used for constants/colors if needed).
* @param {p5.Graphics} b - The p5.Graphics buffer to draw on.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawStaticRegionsToBuffer(p, b, plotScales) {
try {
b.clear();
// 1. Draw Axes (Grid)
// We pass 'b' as the p5 instance so it draws to the buffer.
// Note: drawAxes applies its own coordinate transformations (translate/scale) internally
// but it expects to start from the top-left relative to the canvas.
// However, inside drawAxes it does: p.translate(5, y * scale)...
// AND logic for flipping.
// Let's look at how drawAxes is implemented. It pushes/pops and assumes
// it's drawing in SCREEN coordinates (pixels), but then uses plotScales.
// The main draw loop applies: p.translate(width/2, height*0.95); p.scale(1, -1);
// BEFORE calling drawAxes.
// So 'b' needs to be in that state before we call drawAxes/drawEgoVehicle.
b.push();
// Translate to the bottom center of the buffer.
b.translate(b.width / 2, b.height * 0.95);
// Flip the Y-axis to match radar coordinates (Y increases upwards).
b.scale(1, -1);
// Set stroke properties for the static region lines.
// Draw Axes
drawAxes(b, plotScales); // Pass 'b' as the drawing context
// Draw Ego Vehicle
drawEgoVehicle(b, plotScales); // Pass 'b' as the drawing context
// 2. Draw Static Regions (Original Logic)
b.stroke(100, 100, 100, 150);
b.strokeWeight(1);
// Set dashed line pattern.
b.drawingContext.setLineDash([8, 8]);
// Define angles for the radar beams.
const a1 = p.radians(30),
a2 = p.radians(150);
const a1 = p.radians(30); // Use 'p' for math constants if 'b' lacks them (b usually has them too)
const a2 = p.radians(150);
const len = 70;
// Draw the first static region line.
b.line(
0,
0,
len * p.cos(a1) * plotScales.plotScaleX,
len * p.sin(a1) * plotScales.plotScaleY
len * Math.cos(a1) * plotScales.plotScaleX,
len * Math.sin(a1) * plotScales.plotScaleY
);
// Draw the second static region line.
b.line(
0,
0,
len * p.cos(a2) * plotScales.plotScaleX,
len * p.sin(a2) * plotScales.plotScaleY
len * Math.cos(a2) * plotScales.plotScaleX,
len * Math.sin(a2) * plotScales.plotScaleY
);
// Reset line dash pattern.
b.drawingContext.setLineDash([]);
b.pop();
} catch (error) {
console.error("Error in drawStaticRegionsToBuffer:", error);
}
}
export function drawAxes(p, plotScales) {
try {
p.push();
// Determine axis and text colors based on the current theme (dark/light mode).
const axisColor = document.documentElement.classList.contains("dark")
@ -128,7 +161,7 @@ export function drawAxes(p, plotScales) {
// Draw horizontal grid lines.
p.stroke(axisColor);
p.strokeWeight(1);
for (let y = 5; y <= RADAR_Y_MAX; y += 5)
for (let y = 5; y <= appState.radarYMax; y += 5)
p.line(
RADAR_X_MIN * plotScales.plotScaleX,
y * plotScales.plotScaleY,
@ -147,7 +180,7 @@ export function drawAxes(p, plotScales) {
x * plotScales.plotScaleX,
RADAR_Y_MIN * plotScales.plotScaleY,
x * plotScales.plotScaleX,
RADAR_Y_MAX * plotScales.plotScaleY
appState.radarYMax * plotScales.plotScaleY
);
}
p.stroke(mainAxisColor);
@ -161,13 +194,13 @@ export function drawAxes(p, plotScales) {
0,
RADAR_Y_MIN * plotScales.plotScaleY,
0,
RADAR_Y_MAX * plotScales.plotScaleY
appState.radarYMax * plotScales.plotScaleY
);
// Draw Y-axis labels.
p.fill(textColor);
p.noStroke();
p.textSize(10);
for (let y = 5; y <= RADAR_Y_MAX; y += 5) {
for (let y = 5; y <= appState.radarYMax; y += 5) {
p.push();
p.translate(5, y * plotScales.plotScaleY);
// Flip text vertically to align with flipped Y-axis.
@ -190,12 +223,16 @@ export function drawAxes(p, plotScales) {
p.pop();
}
p.pop();
} catch (error) {
console.error("Error in drawAxes:", error);
}
}
export function drawPointCloud(p, points, plotScales) {
export function drawPointCloud(p, points, plotScales, pointSize = 4) {
try {
// Set stroke weight for points.
p.strokeWeight(4);
p.strokeWeight(pointSize);
// Get state of various toggles from the DOM.
const useSnr = toggleSnrColor.checked;
const useCluster = toggleClusterColor.checked;
@ -209,6 +246,11 @@ export function drawPointCloud(p, points, plotScales) {
if (snrVals.length > 1) {
minSnr = Math.min(...snrVals);
maxSnr = Math.max(...snrVals);
// If all SNR values are the same, add a small range to avoid division by zero
if (minSnr === maxSnr) {
minSnr -= 1;
maxSnr += 1;
}
} else if (snrVals.length === 1) {
minSnr = snrVals[0] - 1;
maxSnr = snrVals[0] + 1;
@ -281,9 +323,13 @@ export function drawPointCloud(p, points, plotScales) {
p.point(pt.x * plotScales.plotScaleX, pt.y * plotScales.plotScaleY);
}
}
} catch (error) {
console.error("Error in drawPointCloud:", error);
}
}
export function drawTrajectories(p, plotScales) {
export function drawTrajectories(p, plotScales, scaleFactor = 1) {
try {
const localTtcColors = ttcColors(p);
for (const track of appState.vizData.tracks) {
@ -300,12 +346,12 @@ export function drawTrajectories(p, plotScales) {
}
const logs = track.historyLog.filter(
(log) => log.frameIdx <= appState.currentFrame + 1
(log) => log.frameIdx <= appState.currentFrame
);
if (logs.length < 2) continue;
const lastLog = logs[logs.length - 1];
if (appState.currentFrame + 1 - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH)
if (appState.currentFrame - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH)
continue;
const isCurrentlyStationary = lastLog.isStationary;
@ -329,7 +375,7 @@ export function drawTrajectories(p, plotScales) {
if (isCurrentlyStationary) {
// Stationary tracks are always green and dashed
p.stroke(34, 139, 34, 220);
p.strokeWeight(1);
p.strokeWeight(1 * scaleFactor);
p.drawingContext.setLineDash([3, 3]);
for (let i = 1; i < trajPts.length; i++) {
// ... (draw fading stationary trajectory logic)
@ -356,11 +402,31 @@ export function drawTrajectories(p, plotScales) {
} else {
// MODE 2: DEFAULT TTC SCHEME (Use pre-calculated category from JSON)
// FIND the TTC category from the new timeline
// 1. Check for 'risk' property (New Logic)
if (lastLog.risk !== undefined && lastLog.risk !== null) {
switch (lastLog.risk) {
case 2: // High Risk
trajectoryColor = p.color(localTtcColors.critical); // Red
break;
case 1: // Medium Risk
trajectoryColor = p.color(localTtcColors.high); // Orange
break;
case 0: // Low Risk
trajectoryColor = p.color(localTtcColors.away); // Blue
break;
default:
trajectoryColor = p.color(localTtcColors.default); // Gray
break;
}
} else {
// 2. Fallback to 'ttcCategoryTimeline' (Old Logic)
let ttcCategory = null;
if (track.ttcCategoryTimeline) {
const ttcEntry = track.ttcCategoryTimeline.find(
(entry) => entry.frameIdx === lastLog.frameIdx
);
const ttcCategory = ttcEntry ? ttcEntry.ttcCategory : null; // Get the category if found
ttcCategory = ttcEntry ? ttcEntry.ttcCategory : null; // Get the category if found
}
switch (ttcCategory) {
case 3:
@ -383,11 +449,13 @@ export function drawTrajectories(p, plotScales) {
break;
}
}
}
p.strokeWeight(1.5);
p.strokeWeight(1.5 * scaleFactor);
p.drawingContext.setLineDash([]);
// Fading trajectory logic (works for both modes)
if (trajPts.length > 0) {
for (let i = 1; i < trajPts.length; i++) {
const alpha = p.map(i, 0, trajPts.length, 50, 255);
trajectoryColor.setAlpha(alpha);
@ -402,34 +470,49 @@ export function drawTrajectories(p, plotScales) {
currPt[1] * plotScales.plotScaleY
);
}
}
// --- END: New Dynamic Coloring Logic ---
}
p.pop(); // This was the missing pop call for each trajectory loop
}
} catch (error) {
console.error("Error in drawTrajectories:", error);
}
}
export function drawTrackMarkers(p, plotScales) {
export function drawTrackMarkers(p, plotScales, scaleFactor = 1, showDetailsBox = true) {
try {
const showDetails = toggleVelocity.checked;
const useStationary = toggleStationaryColor.checked;
const textColor = document.documentElement.classList.contains("dark")
? p.color(255)
: p.color(0);
const localStationaryColor = stationaryColor(p);
const localMovingColor = movingColor(p);
// Style constants for the floating tooltips (matching zoomSketch)
const highlightColor = p.color(46, 204, 113);
const bgColor = document.documentElement.classList.contains("dark")
? p.color(20, 20, 30, 220)
: p.color(245, 245, 245, 220);
const defaultTextColor = document.documentElement.classList.contains("dark")
? p.color(230)
: p.color(20);
// Preparation for smart positioning
const labels = [];
// Adjust text size based on zoom (scaleFactor is roughly 1/zoom)
const textSize = 12 * scaleFactor;
const padding = 6 * scaleFactor;
const lineHeight = textSize * 1.2;
p.push();
p.strokeWeight(2 * scaleFactor);
// Set text size once for width measurement
p.textSize(textSize);
for (const track of appState.vizData.tracks) {
// --- START: Add the Same Safeguard Here ---
// This robust check ensures the track and its historyLog are valid before use.
if (toggleConfirmedOnly.checked && track.isConfirmed === false) {
continue;
}
if (!track || !track.historyLog || !Array.isArray(track.historyLog)) {
// We don't need to log a warning here again, as drawTrajectories already did.
// We can just safely skip this malformed track.
continue;
}
// --- END: Add the Same Safeguard Here ---
if (toggleConfirmedOnly.checked && track.isConfirmed === false) continue;
// Robust check for malformed tracks
if (!track || !track.historyLog || !Array.isArray(track.historyLog)) continue;
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame
@ -438,70 +521,213 @@ export function drawTrackMarkers(p, plotScales) {
if (log) {
const pos = log.correctedPosition;
if (pos && pos.length === 2 && pos[0] !== null && pos[1] !== null) {
const size = 5;
const size = 5 * scaleFactor;
const x = pos[0] * plotScales.plotScaleX;
const y = pos[1] * plotScales.plotScaleY;
let velocityColor = p.color(255, 0, 255, 200);
p.push();
p.strokeWeight(2);
// --- Draw Marker Shape ---
if (useStationary && log.isStationary === true) {
p.stroke(localStationaryColor);
p.noFill();
p.rectMode(p.CENTER);
p.square(x, y, size * 1.5);
velocityColor = localStationaryColor;
} else {
let markerColor = p.color(0, 0, 255);
if (useStationary && log.isStationary === false) {
markerColor = localMovingColor;
velocityColor = localMovingColor;
}
p.stroke(markerColor);
p.line(x - size, y, x + size, y);
p.line(x, y - size, x, y + size);
}
p.pop();
// --- Velocity Vector & Collect Label Data ---
if (
showDetails &&
log.predictedVelocity &&
log.predictedVelocity[0] !== null
) {
const [vx, vy] = log.predictedVelocity;
// Draw velocity line
if (log.isStationary === false) {
p.push();
let velocityColor = p.color(255, 0, 255, 200);
if (useStationary) velocityColor = localMovingColor;
p.stroke(velocityColor);
p.strokeWeight(2);
p.line(
x,
y,
(pos[0] + vx) * plotScales.plotScaleX,
(pos[1] + vy) * plotScales.plotScaleY
);
// Reduce thickness by 25% (2.0 -> 1.5)
p.strokeWeight(1.5 * scaleFactor);
// Make velocity line 30% smaller
const velScale = 0.7;
const vxScaled = vx * velScale;
const vyScaled = vy * velScale;
const endX = (pos[0] + vxScaled) * plotScales.plotScaleX;
const endY = (pos[1] + vyScaled) * plotScales.plotScaleY;
p.line(x, y, endX, endY);
// Draw arrow head
const arrowSize = 4 * scaleFactor;
const angle = Math.atan2(endY - y, endX - x);
p.push();
p.translate(endX, endY);
p.rotate(angle);
// Arrowhead wings
p.line(0, 0, -arrowSize, -arrowSize * 0.6);
p.line(0, 0, -arrowSize, arrowSize * 0.6);
p.pop();
}
const speed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1);
const ttc =
log.ttc !== null && isFinite(log.ttc) && log.ttc < 100
? `TTC: ${log.ttc.toFixed(1)}s`
: "";
const text = `ID: ${track.id} | ${speed} km/h\n${ttc}`;
// --- Collect Text Data (Only if details box is enabled) ---
if (showDetailsBox) {
const speed = (Math.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1);
let ttcText = "";
if ("tti" in log) {
const tti = log.tti;
if (typeof tti === "number" && isFinite(tti)) {
ttcText = `TTI: ${tti.toFixed(1)}s`;
}
} else if (log.ttc !== null && isFinite(log.ttc) && log.ttc < 100) {
ttcText = `TTC: ${log.ttc.toFixed(1)}s`;
}
const risk = getTrackRisk(track, log);
if (risk !== null) {
ttcText += ttcText ? ` | Risk: ${risk}` : `Risk: ${risk}`;
}
const state = log.state !== undefined && log.state !== null ? log.state : track.state;
if (state !== undefined && state !== null) {
ttcText += ttcText ? ` | St: ${state}` : `St: ${state}`;
}
const lines = [`ID: ${track.id} | ${speed} km/h`];
if (ttcText) lines.push(ttcText);
let maxW = 0;
for(let l of lines) maxW = Math.max(maxW, p.textWidth(l));
const w = maxW + padding * 2;
const h = lines.length * lineHeight + padding * 2;
labels.push({ x, y, w, h, lines });
}
}
}
}
}
p.pop(); // End shape drawing context
// --- Smart Positioning & Drawing Labels ---
if (labels.length > 0) {
// Sort by Y descending (Top to Bottom in World Space)
// allowing us to stack labels downwards
labels.sort((a, b) => b.y - a.y);
const placedBoxes = [];
// Increased distance to 60 (3x previous 20)
const offsetDist = 60 * scaleFactor;
for (const label of labels) {
// Initial Position:
// If X < 0: Place to Left (x - offset - width)
// If X >= 0: Place to Right (x + offset)
let bx;
if (label.x < 0) {
bx = label.x - offsetDist - label.w;
} else {
bx = label.x + offsetDist;
}
// Vertical position (Top edge) starts at same Y as marker + offset (Diagonal Up)
let by = label.y + offsetDist;
// Collision Resolution (Greedy)
const maxAttempts = 20;
let attempts = 0;
let collision = true;
while(collision && attempts < maxAttempts) {
collision = false;
for (const pBox of placedBoxes) {
// Check intersection in World Space
// Box A (Current): [bx, bx+w] x [by-h, by]
// Box B (Placed): [pBox.x, pBox.x+pBox.w] x [pBox.y-pBox.h, pBox.y]
const Ax1 = bx, Ax2 = bx + label.w;
const Ay1 = by - label.h, Ay2 = by;
const Bx1 = pBox.x, Bx2 = pBox.x + pBox.w;
const By1 = pBox.y - pBox.h, By2 = pBox.y;
// Standard AABB Intersection
if (Ax1 < Bx2 && Ax2 > Bx1 && Ay1 < By2 && Ay2 > By1) {
// Collision! Move 'by' DOWN (decrease Y)
// Snap Top (by) to just below Placed Box Bottom (By1)
by = By1 - 5 * scaleFactor;
collision = true;
break; // Restart collision check against all
}
}
attempts++;
}
label.finalX = bx;
label.finalY = by;
placedBoxes.push(label);
}
// --- Draw Tooltips ---
for (const label of placedBoxes) {
p.push();
p.fill(textColor);
p.noStroke();
// 1. Draw Leader Line (World Space)
p.stroke(highlightColor);
p.strokeWeight(1 * scaleFactor);
// Draw to the closest side of the box
// If box is to the right, draw to Left Edge (finalX)
// If box is to the left, draw to Right Edge (finalX + w)
let boxSideX;
if (label.finalX > label.x) {
boxSideX = label.finalX; // Box is to the right
} else {
boxSideX = label.finalX + label.w; // Box is to the left
}
const boxCenterY = label.finalY - label.h / 2;
p.line(label.x, label.y, boxSideX, boxCenterY);
// 2. Draw Box & Text
// Translate to Top-Left of box
p.translate(label.finalX, label.finalY);
// Flip for text drawing (local +Y is Down)
p.scale(1, -1);
p.textSize(12);
p.text(text, x + 10, -y);
p.pop();
p.fill(bgColor);
p.stroke(highlightColor);
p.strokeWeight(1 * scaleFactor);
p.rect(0, 0, label.w, label.h, 4 * scaleFactor);
p.noStroke();
p.fill(defaultTextColor);
p.textAlign(p.LEFT, p.TOP);
for(let i=0; i<label.lines.length; i++) {
p.text(label.lines[i], padding, padding + i * lineHeight);
}
p.pop();
}
}
} catch (error) {
console.error("Error in drawTrackMarkers:", error);
}
}
export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
try {
// --- Step 1: Gather Hovered Items ---
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return []; // Return empty array if no data
@ -601,6 +827,9 @@ export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
type: "track",
data: currentLog, // Use the log for the current frame
trackId: track.id,
sign: track.sign,
risk: getTrackRisk(track, currentLog),
state: currentLog.state !== undefined && currentLog.state !== null ? currentLog.state : track.state,
screenX,
screenY,
});
@ -683,9 +912,10 @@ export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
// Calculate speed in km/h, similar to drawTrackMarkers
trackSpeed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1) + " km/h";
}
const signText = item.sign ? ` | Sign: ${item.sign}` : "";
infoText = `Track ${item.trackId} | X:${trackX.toFixed(
2
)}, Y:${trackY.toFixed(2)} | Speed: ${trackSpeed}`;
)}, Y:${trackY.toFixed(2)} | Speed: ${trackSpeed}${signText}`;
// Check for dark mode to ensure visibility
const isDark = document.documentElement.classList.contains("dark");
itemColor = isDark
@ -726,14 +956,21 @@ export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
const xOffset = 20;
let boxX, lineAnchorX;
if (mouseX + xOffset + boxWidth > p.width) { // Use smoothed values
boxX = mouseX - boxWidth - xOffset;
lineAnchorX = boxX + boxWidth;
} else {
// Strategy: Try placing on the right. If it overflows, try the left. If it still overflows, clamp it to screen edges.
if (mouseX + xOffset + boxWidth <= p.width) {
boxX = mouseX + xOffset;
lineAnchorX = boxX;
} else if (mouseX - xOffset - boxWidth >= 0) {
boxX = mouseX - xOffset - boxWidth;
lineAnchorX = boxX + boxWidth;
} else {
// Doesn't cleanly fit on either side. Clamp it to the canvas bounds.
boxX = Math.max(0, Math.min(mouseX + xOffset, p.width - boxWidth));
// Point the anchor to whichever side of the box is closer to the mouse
lineAnchorX = (boxX + boxWidth / 2 < mouseX) ? boxX + boxWidth : boxX;
}
let boxY = p.mouseY - boxHeight / 2;
let boxY = mouseY - boxHeight / 2;
boxY = p.constrain(boxY, 0, p.height - boxHeight);
const highlightColor = p.color(46, 204, 113);
@ -784,6 +1021,10 @@ export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
// Return the list of hovered items for other functions (like the zoom window) to use.
return hoveredItems;
} catch (error) {
console.error("Error in handleCloseUpDisplay:", error);
return [];
}
}
export function drawCovarianceEllipse(
@ -794,6 +1035,7 @@ export function drawCovarianceEllipse(
plotScales,
isStationary
) {
try {
// Only draw the ellipse for tracks that are not stationary.
if (isStationary) return;
const [radiusA, radiusB] = radii;
@ -814,10 +1056,48 @@ export function drawCovarianceEllipse(
radiusB * 2 * plotScales.plotScaleY // in p5 library expect
);
p.pop();
} catch (error) {
console.error("Error in drawCovarianceEllipse:", error);
}
}
export function drawObjectDimensions(
p,
position,
dims,
angle,
plotScales,
isStationary
) {
try {
if (isStationary) return;
const [dimA, dimB] = dims;
const angledegrees = 90 + angle;
p.push();
p.noFill();
p.stroke(128, 0, 128, 150); // Purple
p.strokeWeight(1);
p.translate(
position[0] * plotScales.plotScaleX,
position[1] * plotScales.plotScaleY
);
p.rotate(p.radians(angledegrees));
p.rectMode(p.CENTER);
p.rect(
0,
0,
dimA * 2 * plotScales.plotScaleX,
dimB * 2 * plotScales.plotScaleY
);
p.pop();
} catch (error) {
console.error("Error in drawObjectDimensions:", error);
}
}
export function drawEgoVehicle(p, plotScales) {
try {
const isDark = document.documentElement.classList.contains("dark");
const carColor = isDark ? p.color(150, 150, 220) : p.color(151, 151, 220);
@ -834,9 +1114,13 @@ export function drawEgoVehicle(p, plotScales) {
p.rect(0, -10, carWidthPixels, carLengthPixels, 5);
p.pop();
} catch (error) {
console.error("Error in drawEgoVehicle:", error);
}
}
export function drawRegionsOfInterest(p, frameData, plotScales) {
try {
// --- THIS CHECK IS ESSENTIAL AND MUST NOT BE REMOVED ---
// It gracefully handles frames that do not have the barrier data.
if (!frameData || !frameData.filtered_barrier_x) {
@ -870,7 +1154,7 @@ export function drawRegionsOfInterest(p, frameData, plotScales) {
left * plotScales.plotScaleX,
ROI_TRACKS_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_TRACKS_Y_MAX * plotScales.plotScaleY
appState.radarYMax * plotScales.plotScaleY
);
// --- Draw Close Region ---
@ -883,9 +1167,13 @@ export function drawRegionsOfInterest(p, frameData, plotScales) {
);
p.pop();
} catch (error) {
console.error("Error in drawRegionsOfInterest:", error);
}
}
export function drawClusterCentroids(p, clustersInput, plotScales) {
export function drawClusterCentroids(p, clustersInput, plotScales, scaleFactor = 1) {
try {
if (!clustersInput) {
return; // Do nothing if there's no cluster data
}
@ -920,9 +1208,9 @@ export function drawClusterCentroids(p, clustersInput, plotScales) {
p.push();
p.stroke(color);
p.strokeWeight(1.5);
p.strokeWeight(1.5 * scaleFactor);
const armLength = 5;
const armLength = 5 * scaleFactor;
p.line(x, y - armLength, x, y + armLength);
p.line(x - armLength, y, x + armLength, y);
@ -942,4 +1230,7 @@ export function drawClusterCentroids(p, clustersInput, plotScales) {
p.pop();
}
}
} catch (error) {
console.error("Error in drawClusterCentroids:", error);
}
}

508
steps/src/fileLoader.js

@ -0,0 +1,508 @@
import { appState } from "./state.js";
import { debugFlags } from "./debug.js";
import { saveFileWithMetadata, loadManualOffset, deleteManualOffset } from "./db.js";
import { parseVisualizationJson } from "./fileParsers.js";
import {
showLoadingModal,
updateLoadingModal,
hideModal,
showModal,
} from "./modal.js";
import {
precomputeRadarVideoSync,
extractTimestampInfo,
parseTimestamp,
} from "./utils.js";
import { resetVisualization } from "./sync.js";
import { radarSketch } from "./p5/radarSketch.js";
import { speedGraphSketch } from "./p5/speedGraphSketch.js";
import { zoomSketch } from "./p5/zoomSketch.js";
import {
videoPlayer,
videoPlaceholder,
canvasPlaceholder,
featureToggles,
speedGraphPlaceholder,
snrMinInput,
snrMaxInput,
autoOffsetIndicator,
offsetInput,
speedSlider,
updatePersistentOverlays,
updateDebugOverlay,
resetUIForNewLoad,
startScreenModal,
} from "./dom.js";
import { forceResyncWithOffset } from "./sync.js";
/**
* This is the main handler for both manual clicks and drag-and-drop.
* It identifies the files and triggers the unified processing pipeline.
*/
export function handleFiles(files, fromCache = false) {
// Identify new files from the input
let incomingJson = null;
let incomingVideo = null;
Array.from(files).forEach((file) => {
if (file.name.endsWith(".json")) {
incomingJson = file;
}
if (file.type.startsWith("video/")) {
incomingVideo = file;
}
});
// If no valid files were dropped, do nothing
if (!incomingJson && !incomingVideo) return;
// Trigger the pipeline with the identified files
processFilePipeline(incomingJson, incomingVideo, fromCache);
}
async function processFilePipeline(jsonFile, videoFile, fromCache) {
// 0. Reset the UI to a clean state before processing anything.
// Pass 'true' if a new video is present, 'false' if we should try to keep the old one.
const isNewVideo = !!videoFile;
resetUIForNewLoad(isNewVideo);
// 1. Show the unified loading modal.
showLoadingModal("Processing files...");
const cachePromises = [];
// --- PART A: Setup Filenames & Cache (Moved Up) ---
if (jsonFile) {
appState.jsonFilename = jsonFile.name;
localStorage.setItem("jsonFilename", appState.jsonFilename);
if (!fromCache) {
const savePromise = saveFileWithMetadata("json", jsonFile).catch((e) =>
console.warn(`Non-blocking cache save failed for JSON:`, e)
);
if (debugFlags.CACHE_BLOCKING) {
await savePromise;
} else {
cachePromises.push(savePromise);
}
}
}
if (videoFile) {
appState.videoFilename = videoFile.name;
localStorage.setItem("videoFilename", appState.videoFilename);
// CRITICAL FIX: Reset the videoMissing flag when a new video is being loaded.
appState.videoMissing = false;
if (!fromCache) {
const savePromise = saveFileWithMetadata("video", videoFile).catch((e) =>
console.warn(`Non-blocking cache save failed for Video:`, e)
);
cachePromises.push(savePromise);
}
}
// --- PART B: Calculate Offset (Moved Up) ---
// Critical: This must run BEFORE JSON parsing so valid start times are available.
await calculateAndSetOffset();
// --- PART C: Handle JSON Parsing ---
if (jsonFile) {
// Reset old visualization data immediately
appState.vizData = null;
// Pause P5 loop to prevent errors while data is missing
if (appState.p5_instance) appState.p5_instance.noLoop();
// Parse JSON
const worker = new Worker("./src/parser.worker.js");
const parsedData = await new Promise((resolve, reject) => {
worker.onmessage = (e) => {
const { type, data, percent, message } = e.data;
if (type === "progress") {
updateLoadingModal(percent * 0.8, `Parsing JSON (${percent}%)...`);
} else if (type === "complete") {
worker.terminate();
resolve(data);
} else if (type === "error") {
worker.terminate();
reject(new Error(message));
}
};
worker.postMessage({ file: jsonFile });
});
// Post-process JSON with correct dates
const result = await parseVisualizationJson(
parsedData,
appState.radarStartTimeMs,
appState.videoStartDate
);
if (result.error) {
hideModal();
showModal(result.error);
return;
}
appState.vizData = result.data;
appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr;
}
// --- PART D: Precompute Sync ---
// Bake the offset into the data (needs vizData from Part C and offset from Part B)
if (appState.vizData) {
precomputeRadarVideoSync(appState.vizData, appState.offset);
}
// --- PART E: Load Video (if new) ---
if (videoFile) {
const videoLoaded = await loadVideo(videoFile);
if (!videoLoaded) {
appState.videoMissing = true;
}
}
// --- PART F: Finalize UI ---
finalizeSetup();
if (!appState.videoMissing) {
updateLoadingModal(100, "Complete!");
setTimeout(() => {
hideModal();
startScreenModal.classList.add("hidden");
}, 300);
}
// Log the results of the non-blocking cache operations once they complete.
if (cachePromises.length > 0) {
Promise.allSettled(cachePromises).then((results) => {
console.log("Non-blocking cache operations finished:", results);
});
}
}
// Encapsulates the specific logic for loading a video file into the player
let retries = 0;
function loadVideo(file, isRetry = false) {
return new Promise(async (resolve) => {
let metadataLoaded = false;
let loadTimeout;
// Before creating a new URL, revoke the old one if it exists.
if (!isRetry && appState.videoObjectUrl) {
URL.revokeObjectURL(appState.videoObjectUrl);
appState.videoObjectUrl = null;
}
const fileURL = isRetry ? videoPlayer.src : URL.createObjectURL(file);
const cleanup = () => {
clearTimeout(loadTimeout);
videoPlayer.removeEventListener("loadedmetadata", onMetadataLoaded);
videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough);
videoPlayer.removeEventListener("error", onError);
};
const onMetadataLoaded = () => {
metadataLoaded = true;
updateLoadingModal(95, "Finalizing visualization...");
};
const onCanPlayThrough = () => {
cleanup();
resolve(true);
};
const handleTimeout = async () => {
if (metadataLoaded) {
console.warn(
"Video 'canplaythrough' event timed out, but 'loadedmetadata' fired. Proceeding with playback."
);
appState.videoReadyByFallback = true;
cleanup();
resolve(true);
} else {
// Neither event fired, video is likely broken.
await hideModal(); // Hide the loading modal first and wait for it to finish.
const choice = await showModal(
"Video is taking too long to load. It might be corrupted.",
true, // isConfirm
{ ok: "Retry", cancel: "Continue without Video" }
);
if (choice) { // Retry
if (retries < debugFlags.VIDEO_LOAD_RETRIES) {
retries++;
console.log(`Retrying video load... (Attempt ${retries})`);
showLoadingModal(`Retrying video load...`);
videoPlayer.load(); // Tell the video element to re-fetch
resolve(loadVideo(file, true)); // Recurse
} else {
await showModal("Video load failed after multiple retries.");
resolve(false); // Failed to load
}
} else { // Continue without video
console.warn("User opted to continue without video.");
cleanup();
// Revoke URL to free memory if we're giving up on it
if (videoPlayer.src.startsWith('blob:')) {
URL.revokeObjectURL(videoPlayer.src);
appState.videoObjectUrl = null; // Clear from state
}
videoPlayer.src = "";
videoPlayer.classList.add("hidden");
videoPlaceholder.classList.remove("hidden");
resolve(false); // Signal that video is not loaded
}
}
};
const onError = async (e) => {
console.error("Video loading error:", e);
cleanup();
await hideModal(); // Await is CRITICAL to prevent a race condition with the next modal.
showModal("Error loading video file. It may be an unsupported format or corrupted.");
resolve(false);
};
// Attach listeners
videoPlayer.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
videoPlayer.addEventListener("canplaythrough", onCanPlayThrough, { once: true });
videoPlayer.addEventListener("error", onError, { once: true });
// Start the timeout
loadTimeout = setTimeout(handleTimeout, debugFlags.VIDEO_LOAD_TIMEOUT);
// Apply source only if it's not a retry
if (!isRetry) {
retries = 0; // Reset retry counter for new files
setupVideoPlayer(fileURL);
}
});
}
function finalizeSetup() {
// CRITICAL FIX: Always reset the visualization state before redrawing.
// This pauses the video and resets the timeline, ensuring a clean slate for the new data.
resetVisualization();
// 1. Manage Placeholders & Visibility
// If we have data (vizData), we show the canvas container.
if (appState.vizData) {
canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden");
} else {
// If there's no viz data (e.g., video-only load), hide the canvas
canvasPlaceholder.style.display = ""; // Show placeholder
featureToggles.classList.add("hidden");
if (appState.p5_instance) {
appState.p5_instance.noLoop();
}
// If we don't have data yet (video only), we might keep the placeholder or show an empty canvas?
// Current behavior: keep placeholder until JSON loads.
}
// 2. Initialize/Update P5 Sketches
// We check if they exist; if not, create them. If they do, they will read the new appState on next draw.
if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch);
} else {
// If it existed, ensure it's up to date.
// CRITICAL: Do NOT call .loop(). The app uses a custom animationLoop in sync.js.
appState.p5_instance.redraw();
}
if (!appState.zoomSketchInstance) {
appState.zoomSketchInstance = new p5(zoomSketch, "zoom-canvas-container");
}
// 3. Setup Speed Graph
if (appState.vizData) {
speedGraphPlaceholder.classList.add("hidden");
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
// Update speed graph with new data + video duration
// Determine the most appropriate duration for the graph's X-axis.
let finalDuration = 0;
let jsonDuration = 0;
// 1. Calculate duration from the JSON data itself as a reliable baseline.
if (appState.vizData.radarFrames && appState.vizData.radarFrames.length > 0) {
const lastFrame = appState.vizData.radarFrames[appState.vizData.radarFrames.length - 1];
jsonDuration = lastFrame.timestamp / 1000.0;
}
// 2. Get video duration, normalizing invalid values.
let videoDuration = appState.videoMissing ? 0 : (videoPlayer.duration || 0);
if (!videoDuration || isNaN(videoDuration) || videoDuration <= 0) {
videoDuration = 0;
}
// 3. Set the graph's duration. Prioritize JSON duration, but clip it
// to the video's duration if a video is present and shorter.
finalDuration = jsonDuration;
if (videoDuration > 0 && jsonDuration > videoDuration) {
finalDuration = jsonDuration;
}
appState.speedGraphInstance.setData(appState.vizData, finalDuration);
appState.speedGraphInstance.redraw();
}
// 4. Update UI Overlays
// Manually update overlays so they are visible immediately.
updatePersistentOverlays(videoPlayer.currentTime);
updateDebugOverlay(videoPlayer.currentTime);
// 5. Update SNR Inputs
if (appState.vizData) {
snrMinInput.value = appState.globalMinSnr.toFixed(1);
snrMaxInput.value = appState.globalMaxSnr.toFixed(1);
}
}
// Sets up the video player with the given file URL.
function setupVideoPlayer(fileURL) {
videoPlayer.src = fileURL;
videoPlayer.classList.remove("hidden");
videoPlaceholder.classList.add("hidden");
videoPlayer.playbackRate = parseFloat(speedSlider.value);
videoPlayer.controls = false;
videoPlayer.muted = false;
// Store the new object URL if it's a blob
if (fileURL.startsWith("blob:")) {
appState.videoObjectUrl = fileURL;
}
}
async function calculateAndSetOffset() {
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
let videoDate = null;
if (videoTimestampInfo) {
videoDate = parseTimestamp(
videoTimestampInfo.timestampStr,
videoTimestampInfo.format
);
appState.videoStartDate = videoDate; // Store for potential future use
}
let jsonDate = null;
if (jsonTimestampInfo) {
jsonDate = parseTimestamp(
jsonTimestampInfo.timestampStr,
jsonTimestampInfo.format
);
}
// 1. Try to load a manually saved offset for this specific file pair.
// We use the JSON filename as the primary key.
const savedOffset = appState.jsonFilename ? await loadManualOffset(appState.jsonFilename) : null;
if (savedOffset !== null) {
console.log(`Applying saved manual offset: ${savedOffset}ms`);
appState.offset = savedOffset;
if (jsonDate) {
appState.radarStartTimeMs = jsonDate.getTime();
}
// Update UI
offsetInput.value = appState.offset;
// Show "Manual" indicator
autoOffsetIndicator.textContent = "Manual";
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-gray-500"; // Gray for manual
autoOffsetIndicator.classList.remove("hidden");
localStorage.setItem("visualizerOffset", appState.offset);
return; // Exit early, skipping auto-calc
}
let calculatedOffset = 0;
// We need both dates to calculate an offset.
if (jsonDate && videoDate) {
appState.radarStartTimeMs = jsonDate.getTime();
const offset = jsonDate.getTime() - videoDate.getTime();
if (isNaN(offset) || Math.abs(offset) > 30000) {
console.warn(`Calculated offset of ${offset}ms is invalid or exceeds 30s threshold. Defaulting to 0.`);
calculatedOffset = 0;
// Show "Default" or "Out of Range" indicator
autoOffsetIndicator.textContent = isNaN(offset) ? "Default" : "Out of Range";
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-yellow-600"; // Dark Yellow
autoOffsetIndicator.classList.remove("hidden");
} else {
calculatedOffset = offset;
// Show "Auto" indicator
autoOffsetIndicator.textContent = "Auto";
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-green-500"; // Green
autoOffsetIndicator.classList.remove("hidden");
console.log(`Auto-calculated offset: ${calculatedOffset} ms`);
}
} else if (jsonDate) {
// If we have JSON but no video, we set start time but offset is 0
appState.radarStartTimeMs = jsonDate.getTime();
// No specific indicator needed for JSON-only default 0, or could show "Default"
autoOffsetIndicator.classList.add("hidden");
}
appState.offset = calculatedOffset;
offsetInput.value = appState.offset;
localStorage.setItem("visualizerOffset", appState.offset);
}
/**
* Re-calculates and applies the automatic offset based on filenames.
* This function is triggered by user actions like clicking the 'Manual' indicator
* or using a keyboard shortcut to revert a manual offset.
*/
export function revertToAutoOffset() {
// 1. Calculate the automatic offset.
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
let calculatedOffset = 0;
let indicatorText = "Default";
let indicatorClass = "text-xs font-bold ml-2 text-yellow-600"; // Default to yellow
if (jsonTimestampInfo && videoTimestampInfo) {
const jsonDate = parseTimestamp(jsonTimestampInfo.timestampStr, jsonTimestampInfo.format);
const videoDate = parseTimestamp(videoTimestampInfo.timestampStr, videoTimestampInfo.format);
if (jsonDate && videoDate) {
const offset = jsonDate.getTime() - videoDate.getTime();
if (isNaN(offset) || Math.abs(offset) > 30000) {
calculatedOffset = 0;
indicatorText = "Out of Range";
} else {
calculatedOffset = offset;
indicatorText = "Auto";
indicatorClass = "text-xs font-bold ml-2 text-green-500";
}
}
}
// 2. Update the input box with the new value.
offsetInput.value = calculatedOffset;
// 3. Delete any saved manual offset so future loads default to "Auto" logic.
if (appState.jsonFilename) {
deleteManualOffset(appState.jsonFilename);
}
// 4. Call the resync function with saveToDb = false.
forceResyncWithOffset(false);
// 5. After resyncing, set the correct indicator text and style.
autoOffsetIndicator.textContent = indicatorText;
autoOffsetIndicator.className = indicatorClass;
autoOffsetIndicator.classList.remove("hidden");
}

40
steps/src/fileParsers.js

@ -55,13 +55,49 @@ export async function parseVisualizationJson(
};
}
// Calculate offset: (Radar Start - Video Start). Defaults to 0 if Video Start is unknown.
let offset = 0;
if (videoStartDate && radarStartTimeMs) {
offset = radarStartTimeMs - videoStartDate.getTime();
}
// Always populate timestampMs (Time relative to video start, in ms)
await processArrayInChunks(vizData.radarFrames, 5000, (chunk) => {
chunk.forEach((frame) => {
frame.timestampMs =
radarStartTimeMs + frame.timestamp - videoStartDate.getTime();
// frame.timestamp is assumed to be ms from the radar log start.
// We add the offset to align it with the video timeline.
frame.timestampMs = frame.timestamp + offset;
});
});
// Calculate interFrameTime for each frame
const radarFrames = vizData.radarFrames;
for (let i = 0; i < radarFrames.length; i++) {
if (i < radarFrames.length - 1) {
radarFrames[i].interFrameTime = radarFrames[i + 1].timestampMs - radarFrames[i].timestampMs;
} else {
// Last frame edge case: set its interFrameTime equal to the previous frame's interFrameTime
if (radarFrames.length > 1) {
radarFrames[i].interFrameTime = radarFrames[i - 1].interFrameTime;
} else {
radarFrames[i].interFrameTime = 0; // Only one frame, so interFrameTime is 0
}
}
}
// --- Pre-calculate Max Window IFT for Smart Zoom (Sliding Window) ---
// This eliminates the need for real-time lookahead scanning in the render loop.
const lookahead = 80;
for (let i = 0; i < radarFrames.length; i++) {
let localMax = 0;
const start = Math.max(0, i - lookahead);
const end = Math.min(radarFrames.length - 1, i + lookahead);
for (let j = start; j <= end; j++) {
const val = radarFrames[j].interFrameTime || 0;
if (val > localMax) localMax = val;
}
radarFrames[i].maxWindowIFT = localMax;
}
let snrValues = [];

201
steps/src/keyboard.js

@ -0,0 +1,201 @@
import { appState } from "./state.js";
import {
playPauseBtn,
videoPlayer,
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleStationaryColor,
themeToggleBtn,
toggleTracks,
toggleVelocity,
toggleCloseUp,
togglePredictedPos,
toggleDebugOverlay,
toggleDebug2Overlay,
collapsibleMenu,
toggleMenuBtn,
closeMenuBtn,
updatePersistentOverlays,
} from "./dom.js";
import { updateFrame, resetVisualization } from "./sync.js";
import { VIDEO_FPS } from "./constants.js";
import { findRadarFrameIndexForTime } from "./utils.js";
function handleKeyDown(event) {
// --- FIX APPLIED HERE ---
// We only want to block shortcuts if the user is actively typing in a text or number input.
// This allows shortcuts to work even when other elements, like the timeline slider, are focused.
const isTextInputFocused =
event.target.tagName === "INPUT" &&
(event.target.type === "text" || event.target.type === "number");
if (isTextInputFocused) {
return;
}
// --- END OF FIX ---
const key = event.key;
// We can add any new shortcut keys to this array.
const recognizedKeys = [
"ArrowRight",
"ArrowLeft",
"ArrowUp",
"ArrowDown",
" ",
"1",
"2",
"3",
"4",
"t",
"d",
"g",
"r",
"p",
"a",
"s",
"m",
"q",
"c",
];
// Keys that function globally, even without loaded data
const globalKeys = ["q", "m"];
if (!recognizedKeys.includes(key)) {
return;
}
// If no data is loaded, block keys unless they are global (like theme or menu)
if (!appState.vizData && !globalKeys.includes(key)) {
return;
}
event.preventDefault();
// --- Spacebar for Play/Pause ---
if (key === " ") {
playPauseBtn.click();
}
// --- Arrow keys for frame-by-frame seeking ---
if (key === "ArrowRight" || key === "ArrowLeft") {
if (appState.isPlaying) {
playPauseBtn.click();
}
let newFrame = appState.currentFrame;
if (key === "ArrowRight") {
newFrame = Math.min(
appState.vizData.radarFrames.length - 1,
appState.currentFrame + 1
);
} else if (key === "ArrowLeft") {
newFrame = Math.max(0, appState.currentFrame - 1);
}
if (newFrame !== appState.currentFrame) {
updateFrame(newFrame, true);
// Manually trigger redraws since the animation loop is paused
// This is the fix to ensure the radar plot updates on seek.
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
}
}
// --- Arrow keys for video frame-by-frame seeking ---
if (key === "ArrowUp" || key === "ArrowDown") {
if (appState.isPlaying) {
playPauseBtn.click(); // Pause playback to allow for precise stepping
}
const frameDuration = 1 / VIDEO_FPS;
let newVideoTime = videoPlayer.currentTime;
if (key === "ArrowUp") {
newVideoTime += frameDuration;
} else if (key === "ArrowDown") {
newVideoTime -= frameDuration;
}
// Clamp the new time to be within the video's bounds
videoPlayer.currentTime = Math.max(
0,
Math.min(newVideoTime, videoPlayer.duration)
);
// Find the corresponding radar frame for the new video time
const newFrameIndex = findRadarFrameIndexForTime(
videoPlayer.currentTime,
appState.vizData
);
// Update the application state, but don't force another video seek
updateFrame(newFrameIndex, false);
// Manually trigger redraws since the animation loop is paused
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
}
// --- Number keys for color modes ---
if (key >= "1" && key <= "4") {
const colorToggles = [
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleStationaryColor,
];
const toggleIndex = parseInt(key) - 1;
if (colorToggles[toggleIndex]) {
colorToggles[toggleIndex].click();
}
}
if (key === "q") {
themeToggleBtn.click();
}
if (key === "t") {
toggleTracks.click();
}
if (key === "d") {
toggleVelocity.click();
}
if (key === "g") {
toggleCloseUp.click();
}
if (key === "r") {
resetVisualization();
}
if (key === "c") {
appState.isRawOnlyMode = !appState.isRawOnlyMode;
if (appState.p5_instance) {
appState.p5_instance.redraw();
}
}
if (key === "p") {
togglePredictedPos.click();
appState.p5_instance.redraw();
}
if (key === "s") {
toggleSnrColor.click();
}
if (key === "a") {
toggleDebugOverlay.click();
toggleDebug2Overlay.click();
updatePersistentOverlays(videoPlayer.currentTime);
// The 'a' key is a shortcut to toggle all debug overlays on/off.
// The `updateDebugOverlay` and `updatePersistentOverlays` functions,
// which are called by the toggle's 'change' event listener,
// already handle the logic for showing/hiding the other overlays.
}
if (key === "m") {
if (collapsibleMenu.classList.contains("-translate-x-full")) {
// If the menu is hidden (closed), trigger a click on the OPEN button.
toggleMenuBtn.click();
} else {
// If the menu is not hidden (it's open), trigger a click on the CLOSE button.
closeMenuBtn.click();
}
}
}
export function initKeyboardShortcuts() {
document.addEventListener("keydown", handleKeyDown);
}

978
steps/src/main.js
File diff suppressed because it is too large
View File

102
steps/src/modal.js

@ -8,16 +8,28 @@ import {
modalProgressContainer,
modalProgressBar,
modalProgressText,
startScreenModal,
startProgressContainer,
startProgressBar,
startProgressText,
startDropZone,
startLoadJsonBtn,
startLoadVideoBtn,
} from "./dom.js";
let modalResolve = null;
export function showModal(message, isConfirm = false) {
export function showModal(
message,
isConfirm = false,
buttonLabels = { ok: "OK", cancel: "Cancel" }
) {
return new Promise((resolve) => {
modalText.textContent = message;
// This line correctly shows the "Cancel" button only when needed.
modalOkBtn.textContent = buttonLabels.ok || "OK";
modalCancelBtn.textContent = buttonLabels.cancel || "Cancel";
modalCancelBtn.classList.toggle("hidden", !isConfirm);
// --- THIS IS THE FIX ---
// This ensures the "OK" button is always visible for this modal.
modalOkBtn.classList.remove("hidden");
@ -31,11 +43,24 @@ export function showModal(message, isConfirm = false) {
modalResolve = resolve;
});
}
// A new function specifically for the loading modal
export function showLoadingModal(message) {
export function showLoadingModal(message, forcePopup = false) {
if (!startScreenModal.classList.contains('hidden') && !forcePopup) {
// Integrated start screen flow
startProgressContainer.classList.remove('hidden');
startProgressBar.style.width = '0%';
startProgressText.textContent = message;
// Optionally disable the interactive elements so users don't multi-click
startDropZone.classList.add('opacity-50', 'pointer-events-none');
startLoadJsonBtn.classList.add('opacity-50', 'pointer-events-none');
startLoadVideoBtn.classList.add('opacity-50', 'pointer-events-none');
} else {
// Fallback generic popup modal flow
modalText.textContent = message;
modalOkBtn.classList.add('hidden');
modalCancelBtn.classList.add('hidden');
modalOkBtn.classList.add('hidden'); // Hide OK button for loading
modalCancelBtn.classList.add('hidden'); // Initially hide cancel button
modalProgressContainer.classList.remove('hidden');
modalProgressBar.style.width = '0%';
modalProgressText.textContent = 'Initializing...';
@ -45,19 +70,78 @@ export function showLoadingModal(message) {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
}
}
// A new function to update the progress bar and text
export function updateLoadingModal(percent, message) {
if (modalProgressBar && modalProgressText) {
const p = Math.max(0, Math.min(100, Math.round(percent))); // Clamp between 0-100
// Update integrated start screen loader if active
if (!startProgressContainer.classList.contains('hidden')) {
startProgressBar.style.width = `${p}%`;
startProgressText.textContent = message;
}
// Update generic modal loader if active
if (!modalProgressContainer.classList.contains('hidden')) {
modalProgressBar.style.width = `${p}%`;
modalProgressText.textContent = message;
}
}
export function runStartupLoader(durationMs = 10000) {
return new Promise((resolve, reject) => {
showLoadingModal("Opening Quick Start Guide...", true);
modalCancelBtn.textContent = "Skip Guide";
modalCancelBtn.classList.remove("hidden"); // Show cancel button for startup loader
const startTime = Date.now();
const intervalMs = 100; // Update frequency
let timerId = null;
const cleanup = () => {
clearInterval(timerId);
hideModal(false); // Use hideModal to clear the progress bar and hide the modal
};
const onCancel = () => {
cleanup();
reject('cancelled');
};
modalCancelBtn.onclick = onCancel; // Use the existing modalCancelBtn
timerId = setInterval(() => {
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, durationMs - elapsed);
const percent = Math.min(100, (elapsed / durationMs) * 100);
updateLoadingModal(percent, `${(remaining / 1000).toFixed(1)}s remaining`);
if (elapsed >= durationMs) {
cleanup();
resolve();
}
}, intervalMs);
});
}
// The hideModal function now also resets the progress bar
export function hideModal(value) {
export function hideModal(value) { // This now returns a promise
return new Promise(resolve => {
// Hide the Integrated Loading elements if active
if (!startProgressContainer.classList.contains('hidden')) {
startProgressContainer.classList.add('hidden');
startProgressBar.style.width = '0%';
startProgressText.textContent = '';
startDropZone.classList.remove('opacity-50', 'pointer-events-none');
startLoadJsonBtn.classList.remove('opacity-50', 'pointer-events-none');
startLoadVideoBtn.classList.remove('opacity-50', 'pointer-events-none');
}
modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95");
setTimeout(() => {
@ -68,7 +152,9 @@ export function hideModal(value) {
modalProgressText.textContent = "";
}
if (modalResolve) modalResolve(value);
resolve(); // Resolve the promise returned by hideModal itself
}, 200);
});
}
// Event listeners remain the same

318
steps/src/p5/radarSketch.js

@ -1,7 +1,7 @@
import { appState } from "../state.js";
import { debugFlags } from "../debug.js";
import {
RADAR_X_MAX,
// Define radar plot boundaries
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
@ -13,7 +13,11 @@ import {
togglePredictedPos,
toggleCovariance,
toggleVelocity,
toggleVehicleDimensions,
toggleClusterColor,
toggleConfirmedOnly,
rangeSlider,
rangeValueDisplay,
} from "../dom.js";
import {
drawStaticRegionsToBuffer,
@ -25,6 +29,7 @@ import {
snrColors,
handleCloseUpDisplay,
drawCovarianceEllipse,
drawObjectDimensions,
ttcColors,
drawRegionsOfInterest,
drawClusterCentroids,
@ -47,6 +52,7 @@ export const radarSketch = function (p) {
// --- START: FPS Calculation Variables ---
let lastFrameTime = 0;
let framesDrawn = 0;
// --- END: FPS Calculation Variables ---
// Helper function to allow other sketches to access the static background
@ -55,6 +61,21 @@ export const radarSketch = function (p) {
};
// Function to calculate scaling factors for radar coordinates to canvas pixels
function calculatePlotScales() {
// --- START: Safety Fallback Logic ---
// Ensure we have valid numbers; if not, revert to constants from constants.js
const xMin = typeof appState.radarXMin === 'number' ? appState.radarXMin : RADAR_X_MIN;
const xMax = typeof appState.radarXMax === 'number' ? appState.radarXMax : RADAR_X_MAX;
const yMin = typeof appState.radarYMin === 'number' ? appState.radarYMin : RADAR_Y_MIN;
const yMax = typeof appState.radarYMax === 'number' ? appState.radarYMax : RADAR_Y_MAX;
const dx = xMax - xMin;
const dy = yMax - yMin;
// Final safety: Prevent division by zero if values are somehow identical or inverted
const safeDx = dx > 0 ? dx : 50; // Default width 50m
const safeDy = dy > 0 ? dy : 80; // Default height 80m
// --- END: Safety Fallback Logic ---
// Padding and offset values for the plot area
const hPad = 0.05,
vPad = 0.05,
@ -62,23 +83,34 @@ export const radarSketch = function (p) {
// Calculate available width and height for the plot
const aW = p.width * (1 - 2 * hPad);
const aH = p.height * (1 - bOff - vPad);
// Determine plot scales based on radar boundaries and available canvas space
plotScales.plotScaleX = aW / (RADAR_X_MAX - RADAR_X_MIN);
plotScales.plotScaleY = aH / (RADAR_Y_MAX - RADAR_Y_MIN);
plotScales.plotScaleX = aW / safeDx;
plotScales.plotScaleY = aH / safeDy;
}
p.setup = function () {
// Optimization: Increase target frame rate to match high-refresh monitors.
p.frameRate(144);
// Create the p5.js canvas and attach it to the specified DOM element
let canvas = p.createCanvas(
canvasContainer.offsetWidth,
canvasContainer.offsetHeight
);
canvas.parent("canvas-container");
// --- START: ADD MOUSE WHEEL LISTENER HERE ---
canvas.mouseWheel((event) => {
// --- START: Manual Mouse Wheel Listener for Passive Option ---
// We attach the listener manually to the canvas element to set the `passive` flag to `false`.
// This is necessary to allow `event.preventDefault()` and signals to the browser that
// we are intentionally handling the scroll behavior, resolving the console warning.
canvas.elt.addEventListener(
"wheel",
(event) => {
// Only run this logic if the close-up mode is active
if (appState.isCloseUpMode) {
event.preventDefault(); // Prevent the page from scrolling
// Only allow zooming in god mode if the shift key is NOT pressed.
// When shift is pressed, the scroll event is used for timeline seeking.
if (appState.isCloseUpMode && !event.shiftKey) { event.preventDefault(); // Prevent the page from scrolling
const zoomSpeed = 0.5;
const direction = Math.sign(event.deltaY);
@ -94,10 +126,7 @@ export const radarSketch = function (p) {
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
) {
// We just need to trigger an update; the zoom sketch will read the new
// appState.zoomFactor when it redraws.
// We find the current hovered items again to pass them.
const hoveredItems = handleCloseUpDisplay(p, plotScales);
const hoveredItems = handleCloseUpDisplay(p, plotScales, p.mouseX, p.mouseY);
appState.zoomSketchInstance.updateAndDraw(
p.mouseX,
p.mouseY,
@ -106,8 +135,10 @@ export const radarSketch = function (p) {
);
}
}
});
// --- END: ADD MOUSE WHEEL LISTENER HERE ---
},
{ passive: false }
);
// --- END: Manual Mouse Wheel Listener ---
// Initialize graphics buffers
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
@ -119,24 +150,92 @@ export const radarSketch = function (p) {
p.drawTrackLegendToBuffer(); // Call the new function to draw the legend
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
// Reset FPS state to prevent stale values from previous sessions
appState.fps = 0;
// --- START: Radar Range Slider Logic ---
if (rangeSlider && rangeValueDisplay) {
// Initialize slider value from appState
rangeSlider.value = appState.radarYMax;
rangeValueDisplay.textContent = appState.radarYMax + "m";
// Add listener to handle slider changes
rangeSlider.addEventListener("input", () => {
const newMax = parseInt(rangeSlider.value);
appState.radarYMax = newMax;
rangeValueDisplay.textContent = newMax + "m";
// Recalculate scales and redraw static background
calculatePlotScales();
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
// Redraw main sketch to reflect new scale immediately
p.redraw();
});
// Reset to default on double-click
rangeSlider.addEventListener("dblclick", () => {
appState.radarYMax = RADAR_Y_MAX;
rangeSlider.value = RADAR_Y_MAX;
rangeValueDisplay.textContent = RADAR_Y_MAX + "m";
calculatePlotScales();
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
p.redraw();
});
}
// --- END: Radar Range Slider Logic ---
// --- START: ResizeObserver for GridStack ---
let resizeDebounce = null;
const ro = new ResizeObserver(() => {
// Debounce to prevent massive memory/CPU spikes during fast dragging
if (resizeDebounce) clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
if (canvasContainer && canvasContainer.offsetWidth > 0 && canvasContainer.offsetHeight > 0) {
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
}
}, 100);
});
ro.observe(canvasContainer);
// --- END: ResizeObserver for GridStack ---
p.noLoop();
// Disable continuous looping, redraw will be called manually
};
p.draw = function () {
if (debugFlags.drawing) {
console.log(`[${performance.now().toFixed(3)}] draw_DEBUG: radarSketch.draw() called.`);
}
// --- START: FPS Calculation & Display ---
framesDrawn++;
const currentTime = p.millis();
if (lastFrameTime > 0) {
// Skip FPS calculation during the first few frames to avoid initialization spikes.
// This prevents the "300+ FPS" bug caused by the race between auto-draw and first redraw.
if (framesDrawn < 10) {
lastFrameTime = currentTime;
} else {
const delta = currentTime - lastFrameTime;
if (delta > 0) {
const currentFps = 1000 / delta;
// Use exponential moving average for smoothing
const smoothingFactor = 0.95;
appState.fps =
appState.fps * smoothingFactor + currentFps * (1 - smoothingFactor);
// On the first valid calculation, snap to the current FPS to avoid slow ramp-up.
// Otherwise, use exponential moving average for smoothing.
if (framesDrawn === 10 || appState.fps === 0) {
appState.fps = currentFps;
} else {
// --- START: Frame-Rate Independent FPS Smoothing ---
const baseFactor = 0.05; // Smoothing factor at 60 FPS
const dt = Math.max(0, delta);
const adjustedFactor = 1 - Math.pow(1 - baseFactor, dt / (1000 / 60));
appState.fps = p.lerp(appState.fps, currentFps, adjustedFactor);
// --- END: Frame-Rate Independent FPS Smoothing ---
}
}
lastFrameTime = currentTime;
}
// --- END: FPS Calculation & Display ---
// Set background color based on current theme (dark/light)
@ -149,7 +248,9 @@ export const radarSketch = function (p) {
if (!appState.vizData) return;
// Draw the pre-rendered static background elements
if (staticBackgroundBuffer && staticBackgroundBuffer.width > 0 && staticBackgroundBuffer.height > 0) {
p.image(staticBackgroundBuffer, 0, 0);
}
// Apply transformations for radar coordinate system (origin at bottom-center, Y-axis inverted)
p.push();
@ -158,9 +259,11 @@ export const radarSketch = function (p) {
// Recalculate plot scales (important for window resizing)
calculatePlotScales();
// Draw coordinate axes
drawAxes(p, plotScales);
drawEgoVehicle(p, plotScales);
// --- OPTIMIZATION: Axes and Ego Vehicle are now in staticBackgroundBuffer ---
// drawAxes(p, plotScales);
// drawEgoVehicle(p, plotScales);
// Get current frame data
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (frameData) {
@ -175,15 +278,16 @@ export const radarSketch = function (p) {
// }
if (togglePredictedPos.checked) {
for (const track of appState.vizData.tracks) {
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame
);
if (toggleConfirmedOnly.checked && track.isConfirmed === false) {
continue;
}
const log = track.historyLog.find((log) => log.frameIdx === appState.currentFrame);
if (
log &&
log.predictedPosition &&
log.predictedPosition[0] !== null
) {
const pos = log.predictedPosition;
const pos = log.predictedPosition; //using predicted position from data
const x = pos[0] * plotScales.plotScaleX;
const y = pos[1] * plotScales.plotScaleY;
@ -201,9 +305,11 @@ export const radarSketch = function (p) {
drawTrajectories(p, plotScales);
if (toggleCovariance.checked) {
for (const track of appState.vizData.tracks) {
if (toggleConfirmedOnly.checked && track.isConfirmed === false) {
continue;
}
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame + 1
);
(log) => log.frameIdx === appState.currentFrame);
if (
log &&
log.ellipseRadii &&
@ -223,6 +329,32 @@ export const radarSketch = function (p) {
}
}
}
if (toggleVehicleDimensions.checked) {
for (const track of appState.vizData.tracks) {
if (toggleConfirmedOnly.checked && track.isConfirmed === false) {
continue;
}
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame);
if (
log &&
log.objectExtentRadii &&
typeof log.objectExtentAngle !== "undefined"
) {
const pos = log.correctedPosition;
if (pos && pos[0] !== null) {
drawObjectDimensions(
p,
pos,
log.objectExtentRadii,
log.objectExtentAngle,
plotScales,
log.isStationary
);
}
}
}
}
}
// Draw cluster centroids if enabled
@ -235,7 +367,7 @@ export const radarSketch = function (p) {
// 4. Draw the new legend buffer onto the main canvas
// This is placed at the bottom-right corner.
if (toggleTracks.checked && !appState.isRawOnlyMode) {
if (toggleTracks.checked && !appState.isRawOnlyMode && trackLegendBuffer && trackLegendBuffer.width > 0) {
p.image(
trackLegendBuffer,
p.width - trackLegendBuffer.width - 10,
@ -250,6 +382,15 @@ export const radarSketch = function (p) {
const zoomPanel = document.getElementById("zoom-panel");
if (appState.isCloseUpMode) {
// --- START: Mouse Smoothing Logic ---
// --- START: Cursor Indicator Logic ---
// Change the cursor to provide a visual cue for the current scroll behavior.
if (p.keyIsDown(p.SHIFT)) {
p.cursor("ew-resize"); // Indicates horizontal seeking (timeline scrub)
} else {
p.cursor("crosshair"); // Default for zoom/inspection
}
// --- END: Cursor Indicator Logic ---
// On the first frame of zoom, snap the smoothed position to the real mouse position.
if (isFirstFrame) {
smoothedMouseX = p.mouseX;
@ -257,15 +398,25 @@ export const radarSketch = function (p) {
isFirstFrame = false;
}
// The smoothing factor. A smaller value (e.g., 0.1) means more smoothing.
// This can be adjusted to feel more or less responsive.
const smoothingFactor = 0.5;
// --- START: Frame-Rate Independent Smoothing ---
// We use p.deltaTime to adjust the smoothing factor so that the animation
// speed remains consistent across different monitor refresh rates.
const baseSmoothing = 0.5; // Target smoothing at 60 FPS
const dt = Math.max(0, p.deltaTime);
const adjustedSmoothing = 1 - Math.pow(1 - baseSmoothing, dt / (1000 / 60));
// Linearly interpolate the smoothed position towards the actual mouse position.
smoothedMouseX = p.lerp(smoothedMouseX, p.mouseX, smoothingFactor);
smoothedMouseY = p.lerp(smoothedMouseY, p.mouseY, smoothingFactor);
smoothedMouseX = p.lerp(smoothedMouseX, p.mouseX, adjustedSmoothing);
smoothedMouseY = p.lerp(smoothedMouseY, p.mouseY, adjustedSmoothing);
// --- END: Frame-Rate Independent Smoothing ---
// Use the smoothed coordinates for all subsequent zoom-related calculations.
// Store current transformed mouse coordinates (un-scaled back by original zoom factor)
// because zoomedMouseX is transformed by zoom and translation.
// Easiest is to check actual raw cursor coords on the canvas.
appState.isMouseOutOfBounds = p.mouseX < 0 || p.mouseX > p.width || p.mouseY < 0 || p.mouseY > p.height;
const hoveredItems = handleCloseUpDisplay(p, plotScales, smoothedMouseX, smoothedMouseY);
// --- END: Mouse Smoothing Logic ---
@ -308,18 +459,21 @@ export const radarSketch = function (p) {
p.ellipse(smoothedMouseX, smoothedMouseY, hoverRadius * 2, hoverRadius * 2); // Use smoothed values
p.pop();
// --- END: Draw Zoom Area Rectangle & Debug Circle ---
if (hoveredItems.length > 0) {
// If we are hovering, cancel any existing countdown.
if (appState.zoomHideDelayTimeout) {
clearTimeout(appState.zoomHideDelayTimeout);
appState.zoomHideDelayTimeout = null;
}
if (appState.zoomCountdownInterval) {
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
}
appState.zoomCountdown = null;
if (zoomPanel.style.display !== "block") {
zoomPanel.style.display = "block";
if (zoomPanel && zoomPanel.classList.contains("hidden") && !appState.zoomPanelExplicitlyClosed) {
zoomPanel.classList.remove("hidden");
}
if (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
@ -331,11 +485,30 @@ export const radarSketch = function (p) {
plotScales
);
}
} else if (zoomPanel.style.display === "block") {
} else {
// --- START: FIX for Grace Period Freeze ---
// If NOT hovering, but the panel is still visible, we must continue
// to update the zoom sketch so it follows the mouse.
// If NOT hovering, we must continue to update the zoom sketch
// so it follows the mouse.
// We pass an empty array for hoveredItems, so no tooltip is drawn.
// Auto-hide the panel after 5 seconds of inactivity with countdown
if (zoomPanel && !zoomPanel.classList.contains("hidden")) {
if (!appState.zoomHideDelayTimeout && !appState.zoomCountdownInterval) {
appState.zoomHideDelayTimeout = setTimeout(() => {
appState.zoomHideDelayTimeout = null;
appState.zoomCountdown = 3;
appState.zoomCountdownInterval = setInterval(() => {
appState.zoomCountdown--;
if (appState.zoomCountdown <= 0) {
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
appState.zoomCountdown = null;
zoomPanel.classList.add("hidden");
}
}, 1000);
}, 5000);
}
}
if (
appState.zoomSketchInstance &&
appState.zoomSketchInstance.updateAndDraw
@ -348,40 +521,11 @@ export const radarSketch = function (p) {
);
}
// --- END: FIX for Grace Period Freeze ---
// 2. If a "hide" timer isn't already running, start one.
if (!appState.zoomHideDelayTimeout && !appState.zoomCountdownInterval) {
// Start a 2-second delay before the countdown begins.
appState.zoomHideDelayTimeout = setTimeout(() => {
appState.zoomHideDelayTimeout = null; // Clear the delay timer ID
// Now, start the actual 3-second countdown interval.
appState.zoomCountdown = Math.floor(COOLING_PERIOD_MS / 1000);
appState.zoomCountdownInterval = setInterval(() => {
appState.zoomCountdown--;
if (appState.zoomCountdown <= 0) {
// When countdown finishes, hide panel and clear interval.
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
appState.zoomCountdown = null;
zoomPanel.style.display = "none";
} else {
// Force a redraw of the zoom sketch to show the new countdown value.
// This call is still needed inside the interval to update the countdown text.
if (appState.zoomSketchInstance && appState.zoomSketchInstance.updateAndDraw) {
// Pass empty hoveredItems to show the countdown text.
appState.zoomSketchInstance.updateAndDraw(
smoothedMouseX,
smoothedMouseY,
[],
plotScales);
}
}
}, 1000);
}, 1000); // 1000ms = 1 second delay
}
}
} else {
// --- START: Cleanup Logic ---
// When zoom mode is turned off, ensure all timers are cleared.
p.cursor(p.AUTO); // Reset cursor when exiting god mode
// Clear timers when explicitly turned off
if (appState.zoomHideDelayTimeout) {
clearTimeout(appState.zoomHideDelayTimeout);
appState.zoomHideDelayTimeout = null;
@ -390,13 +534,13 @@ export const radarSketch = function (p) {
clearInterval(appState.zoomCountdownInterval);
appState.zoomCountdownInterval = null;
}
// --- END: Cleanup Logic ---
zoomPanel.style.display = "none";
appState.zoomCountdown = null;
isFirstFrame = true; // Reset for the next time zoom mode is enabled
}
// --- Legend Drawing ---
// Draw the SNR legend if enabled
if (toggleSnrColor.checked) {
// Draw the legend buffer if requested
if (toggleSnrColor.checked && snrLegendBuffer && snrLegendBuffer.width > 0) {
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10);
}
};
@ -458,27 +602,25 @@ export const radarSketch = function (p) {
p.windowResized = function () {
console.log("radarSketch: windowResized triggered!");
p.windowResized = function () {}; // Disable native p5 window event to prevent multi-monitor dragging double-fires
p.handleContainerResize = function () {
console.log("radarSketch: handleContainerResize triggered!");
// Immediately resize the elements that we know are stable.
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
// PREVENT MEMORY LEAK: Destroy old buffers to free memory
if (staticBackgroundBuffer) staticBackgroundBuffer.remove();
if (trackLegendBuffer) trackLegendBuffer.remove();
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
trackLegendBuffer = p.createGraphics(120, 120);
p.drawTrackLegendToBuffer();
calculatePlotScales();
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
// Defer the call to destroy the zoom canvas.
if (appState.zoomSketchInstance && appState.isCloseUpMode) {
setTimeout(() => {
console.log(
"radarSketch: Executing deferred call to zoomSketch.handleResize()."
);
appState.zoomSketchInstance.handleResize();
}, 10); // A 10ms delay is slightly more robust than 0.
}
if (appState.vizData) {
p.redraw();
}

387
steps/src/p5/speedGraphSketch.js

@ -1,12 +1,44 @@
// File: src/speedGraphSketch.js
import { appState } from "../state.js";
import { videoPlayer, speedGraphContainer } from "../dom.js";
import { videoPlayer, speedGraphContainer, playPauseBtn } from "../dom.js";
import { updateFrame, pausePlayback } from "../sync.js";
import { ttcColors } from "../drawUtils.js";
import { debugFlags } from "../debug.js";
export const speedGraphSketch = function (p) {
let staticBuffer, minSpeed, maxSpeed, videoDuration;
// Reserve more top space for legend and reduce the right padding so the plot can use more width.
const pad = { top: 48, right: 20, bottom: 30, left: 50 };
// Hover state
let hoverX = null;
let hoverTimeSec = null;
let hoverFrameIndex = null;
let hoverCanSpeed = null;
let hoverEgoSpeed = null;
const tooltipPadding = 8;
let hoverTimeout = null; // To manage the hover-off delay
let isMouseOver = false; // To track if the mouse is on the canvas
function findNearestFrameIndexByTime(ms) {
if (!appState.vizData || !appState.vizData.radarFrames) return null;
const frames = appState.vizData.radarFrames;
let lo = 0, hi = frames.length - 1;
if (frames.length === 0) return null;
if (ms <= frames[0].timestamp) return 0;
if (ms >= frames[hi].timestamp) return hi;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const t = frames[mid].timestamp;
if (t === ms) return mid;
if (t < ms) lo = mid + 1; else hi = mid - 1;
}
// after loop, lo is the first index greater than ms; choose nearest of lo and lo-1
const idxA = Math.max(0, lo - 1);
const idxB = Math.min(frames.length - 1, lo);
return (Math.abs(frames[idxA].timestamp - ms) <= Math.abs(frames[idxB].timestamp - ms)) ? idxA : idxB;
}
p.drawStaticGraphToBuffer = function (radarData) {
const b = staticBuffer;
b.clear();
@ -15,6 +47,64 @@ export const speedGraphSketch = function (p) {
const gridColor = isDark ? 100 : 200;
const textColor = isDark ? 200 : 100;
// --- Step 1: Define Spectral Color Scheme (MATLAB Style) ---
// Anchors: Blue (0%) -> Cyan (25%) -> Green (50%) -> Yellow (75%) -> Red (100%)
const spectralAnchors = [
p.color(0, 0, 255), // Blue
p.color(0, 255, 255), // Cyan
p.color(0, 255, 0), // Green
p.color(255, 255, 0), // Yellow
p.color(255, 0, 0) // Red
];
function getSpectralColor(ratio) {
const amt = p.constrain(ratio, 0, 1);
if (amt <= 0.25) return p.lerpColor(spectralAnchors[0], spectralAnchors[1], amt / 0.25);
if (amt <= 0.50) return p.lerpColor(spectralAnchors[1], spectralAnchors[2], (amt - 0.25) / 0.25);
if (amt <= 0.75) return p.lerpColor(spectralAnchors[2], spectralAnchors[3], (amt - 0.50) / 0.25);
return p.lerpColor(spectralAnchors[3], spectralAnchors[4], (amt - 0.75) / 0.25);
}
// --- Step 2: Pre-calculate Track Density ---
const numFrames = radarData && radarData.radarFrames ? radarData.radarFrames.length : 0;
const trackCounts = new Uint16Array(numFrames).fill(0);
const confirmedOnly = document.getElementById("toggleConfirmedOnly")?.checked ?? true;
if (radarData && radarData.tracks && numFrames > 0) {
for (const track of radarData.tracks) {
// Only count tracks that would actually be visible in the confirmed view
if (confirmedOnly && track.isConfirmed === false) continue;
if (track.historyLog) {
for (const log of track.historyLog) {
if (log.frameIdx >= 0 && log.frameIdx < numFrames) {
trackCounts[log.frameIdx]++;
}
}
}
}
}
// Determine normalization factor using a robust metric (95th percentile)
// This prevents a single frame with 100 tracks (noise) from making the rest of the graph blue.
let normTracks = 1;
if (numFrames > 0) {
const sortedCounts = [...trackCounts].sort((a, b) => a - b);
// Use 95th percentile as the "High" anchor
const p95Index = Math.floor(numFrames * 0.95);
const p95Value = sortedCounts[p95Index];
const maxValue = sortedCounts[numFrames - 1];
// We'll normalize against p95, but ensure it's at least a reasonable number.
normTracks = Math.max(1, p95Value);
if (debugFlags.speedGraph) {
console.log(`[SpeedGraph] Density Info (Confirmed Only: ${confirmedOnly}):`);
console.log(` - Max tracks: ${maxValue}, 95th Percentile: ${p95Value}`);
console.log(` - Normalizing against: ${normTracks}`);
}
}
b.push();
b.stroke(gridColor);
b.strokeWeight(1);
@ -28,7 +118,7 @@ export const speedGraphSketch = function (p) {
b.fill(textColor);
b.textSize(10);
for (let s = minSpeed; s <= maxSpeed; s += 10) {
for (let s = minSpeed; s <= maxSpeed; s += 5) {
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.text(s, pad.left - 8, y);
if (s === 0) {
@ -43,7 +133,7 @@ export const speedGraphSketch = function (p) {
}
b.fill(textColor);
b.text("km/h", pad.left - 8, pad.top - 8);
b.text("km/h", pad.left - 8, pad.top - 12);
b.textAlign(b.CENTER, b.TOP);
b.noStroke();
b.fill(isDark ? 180 : 150);
@ -54,25 +144,81 @@ export const speedGraphSketch = function (p) {
b.text(Math.round(t), x, b.height - pad.bottom + 5);
}
b.fill(textColor);
// Draw vertical grid lines for time
b.strokeWeight(1);
b.stroke(isDark ? 80 : 230);
for (let t = 10; t <= videoDuration; t += 10) {
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right);
b.line(x, pad.top, x, b.height - pad.bottom);
}
b.noStroke();
b.text("Time (s)", (pad.left + (b.width - pad.right)) / 2, b.height - pad.bottom + 18);
b.pop();
// Draw CAN speed (solid blue)
// --- Density Legend Bar (Left Side) ---
// Smooth gradient representation of track density
const lx = 10;
const lw = 6;
const ly = pad.top;
const lh = b.height - pad.bottom - pad.top;
b.push();
b.noFill();
for (let i = 0; i < lh; i++) {
const ratio = b.map(i, 0, lh, 1, 0); // 1 at top (red), 0 at bottom (blue)
b.stroke(getSpectralColor(ratio));
b.line(lx, ly + i, lx + lw, ly + i);
}
b.pop();
// Legend Labels for the vertical bar
b.fill(textColor);
b.textSize(9);
b.textAlign(b.LEFT, b.TOP);
b.text(normTracks, lx + lw + 3, ly);
b.textAlign(b.LEFT, b.BOTTOM);
b.text("0", lx + lw + 3, ly + lh);
b.textAlign(b.LEFT, b.TOP);
b.text("Tracks", lx, ly + lh + 4);
// Draw CAN speed (Colored by Track Density)
if (radarData && radarData.radarFrames) {
b.strokeWeight(2.5); // Slightly thicker for better color visibility
b.noFill();
b.stroke(0, 150, 255);
b.strokeWeight(1.5);
b.beginShape();
for (const frame of radarData.radarFrames) {
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) continue;
const relTime = frame.timestampMs / 1000;
if (relTime >= 0 && relTime <= videoDuration) {
let prevX = null;
let prevY = null;
for (let i = 0; i < radarData.radarFrames.length; i++) {
const frame = radarData.radarFrames[i];
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) {
prevX = null;
continue;
}
const relTime = frame.timestamp / 1000;
if (relTime < 0 || relTime > videoDuration) continue;
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right);
const y = b.map(frame.canVehSpeed_kmph, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.vertex(x, y);
const speed = frame.canVehSpeed_kmph;
const y = b.map(speed, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
if (prevX !== null) {
// Robust normalization: Ratio based on 95th percentile
const ratio = trackCounts[i] / normTracks;
b.stroke(getSpectralColor(ratio));
b.line(prevX, prevY, x, y);
}
prevX = x;
prevY = y;
}
b.endShape();
}
// Draw Ego speed (dashed green)
@ -81,7 +227,7 @@ export const speedGraphSketch = function (p) {
b.drawingContext.setLineDash([5, 5]);
b.beginShape();
for (const frame of radarData.radarFrames) {
const relTime = frame.timestampMs / 1000;
const relTime = frame.timestamp / 1000;
if (relTime >= 0 && relTime <= videoDuration) {
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right);
const egoSpeedKmh = frame.egoVelocity[1] * 3.6;
@ -100,7 +246,7 @@ export const speedGraphSketch = function (p) {
b.textSize(12);
b.textAlign(b.LEFT, b.CENTER);
const canLabel = "CAN Speed";
const canLabel = "CAN Speed (Color: Tracks Density)";
const egoLabel = "Ego Speed";
const segLen = 18;
@ -119,11 +265,17 @@ export const speedGraphSketch = function (p) {
const legendStartX = centerX - totalLegendWidth / 2;
const legendY = pad.top / 2; // vertically centered inside the top padding
// Draw CAN legend item
b.push();
b.stroke(0, 150, 255);
// Draw CAN legend item (Gradient Line to represent density range)
// We draw small segments of different colors to show the range
b.strokeWeight(2);
b.line(legendStartX, legendY + 6, legendStartX + segLen, legendY + 6);
const step = segLen / 5;
// Use spectralAnchors for the horizontal legend line
b.stroke(spectralAnchors[0]); b.line(legendStartX, legendY + 6, legendStartX + step, legendY + 6);
b.stroke(spectralAnchors[1]); b.line(legendStartX + step, legendY + 6, legendStartX + step*2, legendY + 6);
b.stroke(spectralAnchors[2]); b.line(legendStartX + step*2, legendY + 6, legendStartX + step*3, legendY + 6);
b.stroke(spectralAnchors[3]); b.line(legendStartX + step*3, legendY + 6, legendStartX + step*4, legendY + 6);
b.stroke(spectralAnchors[4]); b.line(legendStartX + step*4, legendY + 6, legendStartX + segLen, legendY + 6);
b.noStroke();
b.fill(textColor);
b.text(canLabel, legendStartX + segLen + gapBetweenSegAndLabel, legendY + 6);
@ -141,19 +293,140 @@ export const speedGraphSketch = function (p) {
b.fill(textColor);
b.text(egoLabel, egoX + segLen + gapBetweenSegAndLabel, legendY + 6);
b.pop();
b.pop();
};
let isDragging = false;
function updateHoverState(x) {
if (!appState.vizData || !appState.vizData.radarFrames || videoDuration === undefined) {
hoverX = null;
return;
}
// Clamp x to the valid plotting width for calculation
hoverX = Math.max(pad.left, Math.min(p.width - pad.right, x));
// map hoverX to time in seconds inside [0, videoDuration]
const dur = videoDuration > 0 ? videoDuration : Math.max(1, (appState.vizData.radarFrames[appState.vizData.radarFrames.length - 1].timestamp / 1000));
hoverTimeSec = p.map(hoverX, pad.left, p.width - pad.right, 0, dur);
// Clamp time to [0, duration]
hoverTimeSec = Math.max(0, Math.min(dur, hoverTimeSec));
const hoverTimeMs = Math.round(hoverTimeSec * 1000);
hoverFrameIndex = findNearestFrameIndexByTime(hoverTimeMs);
if (hoverFrameIndex !== null) {
const f = appState.vizData.radarFrames[hoverFrameIndex];
hoverCanSpeed = (f.canVehSpeed_kmph !== null && !isNaN(f.canVehSpeed_kmph)) ? f.canVehSpeed_kmph : null;
hoverEgoSpeed = f.egoVelocity ? (f.egoVelocity[1] * 3.6) : null; // convert m/s to km/h
} else {
hoverCanSpeed = null;
hoverEgoSpeed = null;
}
}
p.setup = function () {
let canvas = p.createCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight);
canvas.parent("speed-graph-container");
// --- Pointer Events for Drag & Click ---
canvas.elt.addEventListener('pointerdown', (e) => {
if (!appState.vizData) return;
isDragging = true;
canvas.elt.setPointerCapture(e.pointerId);
if (appState.isPlaying) {
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
// Instant seek on click
updateHoverState(e.offsetX);
if (hoverFrameIndex !== null) {
updateFrame(hoverFrameIndex, false);
if (appState.p5_instance) appState.p5_instance.redraw();
p.redraw();
}
});
canvas.elt.addEventListener('pointermove', (e) => {
if (!appState.vizData) return;
if (isDragging) {
// When dragging, clamp X to canvas bounds and seek
const rect = canvas.elt.getBoundingClientRect();
// Calculate offsetX manually if needed, or trust e.offsetX with capture
// With setPointerCapture, e.offsetX is relative to the target (canvas).
updateHoverState(e.offsetX);
if (hoverFrameIndex !== null) {
updateFrame(hoverFrameIndex, false);
if (appState.p5_instance) appState.p5_instance.redraw();
}
p.redraw();
} else {
// Normal Hover Behavior
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
// If we are hovering, e.offsetX is correct.
updateHoverState(e.offsetX);
p.redraw();
}
});
canvas.elt.addEventListener('pointerup', (e) => {
if (isDragging) {
isDragging = false;
canvas.elt.releasePointerCapture(e.pointerId);
// Final precise seek (forces video sync)
updateFrame(appState.currentFrame, true);
}
});
// Clear hover state when mouse leaves the canvas (only if not dragging)
canvas.mouseOut(() => {
if (isDragging) return;
hoverTimeout = setTimeout(() => {
hoverX = null;
hoverFrameIndex = null;
hoverCanSpeed = null;
hoverEgoSpeed = null;
p.redraw();
}, 100);
});
staticBuffer = p.createGraphics(p.width, p.height);
// --- START: ResizeObserver for GridStack ---
let resizeDebounce = null;
const ro = new ResizeObserver(() => {
// Debounce to prevent massive memory/CPU spikes during fast dragging
if (resizeDebounce) clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
if (speedGraphContainer && speedGraphContainer.offsetWidth > 0 && speedGraphContainer.offsetHeight > 0) {
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
}
}, 100);
});
ro.observe(speedGraphContainer);
// --- END: ResizeObserver for GridStack ---
p.noLoop();
};
p.setData = function (radarData, duration) {
if (!radarData || !radarData.radarFrames) return;
// Clear the old buffer to prevent showing stale graphs, especially if new data has no duration.
staticBuffer.clear();
p.background(document.documentElement.classList.contains("dark") ? [55, 65, 81] : 255);
videoDuration = duration;
let speeds = [];
@ -172,22 +445,75 @@ export const speedGraphSketch = function (p) {
if (maxSpeed <= 0) maxSpeed = 10;
if (minSpeed >= 0) minSpeed = 0;
if (videoDuration > 0) {
if (videoDuration >= 0) {
p.drawStaticGraphToBuffer(radarData);
}
};
p.draw = function () {
if (!videoDuration || videoDuration <= 0) {
if (!staticBuffer || !videoDuration || videoDuration <= 0) {
const isDark = document.documentElement.classList.contains("dark");
p.background(isDark ? [55, 65, 81] : 255);
p.fill(isDark ? 200 : 100);
p.textAlign(p.CENTER, p.CENTER);
p.text("Waiting for video duration...", p.width / 2, p.height / 2);
p.text("No data to display", p.width / 2, p.height / 2);
return;
}
if (staticBuffer && staticBuffer.width > 0 && staticBuffer.height > 0) {
p.image(staticBuffer, 0, 0);
}
drawTimeIndicator();
// draw hover vertical line and tooltip if applicable
if (hoverX !== null && hoverFrameIndex !== null) {
p.push();
// Draw dashed vertical line
p.stroke(255, 0, 255, 200); // Fuschia
p.strokeWeight(1.2);
p.drawingContext.setLineDash([4, 4]);
p.line(hoverX, pad.top, hoverX, p.height - pad.bottom);
p.drawingContext.setLineDash([]); // Reset to solid
p.noStroke();
// Draw blue circle for CAN speed at hover point
if (hoverCanSpeed !== null) {
const y = p.map(hoverCanSpeed, minSpeed, maxSpeed, p.height - pad.bottom, pad.top);
p.fill(255, 0, 255); // Same blue color
p.noStroke();
p.ellipse(hoverX, y, 8, 8);
}
// Tooltip content
const canText = hoverCanSpeed !== null ? `CAN: ${hoverCanSpeed.toFixed(1)} km/h` : `CAN: N/A`;
const egoText = hoverEgoSpeed !== null ? `Ego: ${hoverEgoSpeed.toFixed(1)} km/h` : `Ego: N/A`;
const timeText = `t=${hoverTimeSec !== null ? hoverTimeSec.toFixed(2) + ' s' : ''}`;
const tooltipLines = [timeText, canText, egoText];
const textWidthMax = Math.max(...tooltipLines.map((t) => p.textWidth(t)));
const boxW = textWidthMax + tooltipPadding * 2;
const boxH = (tooltipLines.length * 14) + tooltipPadding * 2;
// compute box position (avoid overflowing right edge)
let boxX = hoverX + 12;
if (boxX + boxW > p.width) boxX = hoverX - 12 - boxW;
boxX = Math.max(0, boxX); // avoid overflowing left edge
const boxY = pad.top + 6;
// Draw background box
p.fill(document.documentElement.classList.contains("dark") ? 40 : 255);
p.stroke(document.documentElement.classList.contains("dark") ? 180 : 80);
p.rect(boxX, boxY, boxW, boxH, 6);
p.noStroke();
p.fill(document.documentElement.classList.contains("dark") ? 220 : 30);
p.textSize(12);
p.textAlign(p.LEFT, p.TOP);
for (let i = 0; i < tooltipLines.length; i++) {
p.text(tooltipLines[i], boxX + tooltipPadding, boxY + tooltipPadding + i * 14);
}
p.pop();
}
};
function drawTimeIndicator() {
@ -203,7 +529,7 @@ export const speedGraphSketch = function (p) {
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return;
const currentTimeSec = frameData.timestampMs / 1000.0;
const currentTimeSec = frameData.timestamp / 1000.0;
const x = p.map(currentTimeSec, 0, videoDuration, pad.left, p.width - pad.right);
p.stroke(255, 0, 0, 150);
@ -219,9 +545,16 @@ export const speedGraphSketch = function (p) {
}
}
p.windowResized = function () {
p.windowResized = function () {}; // Disable native p5 window event
p.handleContainerResize = function () {
p.resizeCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight);
// PREVENT MEMORY LEAK: Destroy old buffer before recreating
if (staticBuffer) staticBuffer.remove();
staticBuffer = p.createGraphics(p.width, p.height);
hoverX = null; // reset hover on resize
if (appState.vizData && videoDuration > 0) {
p.drawStaticGraphToBuffer(appState.vizData);
}

248
steps/src/p5/zoomSketch.js

@ -17,8 +17,8 @@ import {
toggleCovariance,
} from "../dom.js";
function drawZoomTooltip(p, hoveredItems, mainMouseX) {
if (!hoveredItems || hoveredItems.length === 0) return;
function drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY, smoothedCamX, smoothedCamY) {
if (!hoveredItems || hoveredItems.length === 0 || smoothedAvgX === null) return;
// 1. Generate text content
const infoStrings = [];
@ -61,9 +61,12 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
trackVy = vy.toFixed(2);
trackSpeed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1) + " km/h";
}
const signText = item.sign ? ` | Sign: ${item.sign}` : "";
const riskText = item.risk !== null && item.risk !== undefined ? ` | Risk: ${item.risk}` : "";
const stateText = item.state !== null && item.state !== undefined ? ` | St: ${item.state}` : "";
infoText = `Track ${item.trackId} | X:${trackX.toFixed(
2
)}, Y:${trackY.toFixed(2)} | Speed: ${trackSpeed}`;
)}, Y:${trackY.toFixed(2)} | Speed: ${trackSpeed}${signText}${riskText}${stateText}`;
const isDark = document.documentElement.classList.contains("dark");
itemColor = isDark
? p.color(100, 149, 237) // Lighter blue for dark mode
@ -91,13 +94,9 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
}
}
// 2. Find the average screen position of hovered items
const avgX =
hoveredItems.reduce((acc, item) => acc + item.screenX, 0) /
hoveredItems.length;
const avgY =
hoveredItems.reduce((acc, item) => acc + item.screenY, 0) /
hoveredItems.length;
// 2. Use filtered screen positions for the tooltip box
const avgX = smoothedAvgX;
const avgY = smoothedAvgY;
p.push();
@ -111,6 +110,7 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
const BASE_HIGHLIGHT_THICKNESS = 2;
const BASE_LINE_THICKNESS = 2;
const BASE_DISTANCE_OFFSET = 65; // <-- How far the tooltip is from the items
const BASE_VERTICAL_OFFSET = 40; // <-- Upward shift for diagonal effect
// COLORS
const highlightColor = p.color(46, 204, 113); // Green for border and lines
@ -127,6 +127,7 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
const lineHeight = BASE_LINE_HEIGHT / zoomFactor;
const boxPadding = BASE_PADDING / zoomFactor;
const xOffset = BASE_DISTANCE_OFFSET / zoomFactor;
const yOffset = BASE_VERTICAL_OFFSET / zoomFactor;
let boxWidth = 0;
infoStrings.forEach((info) => {
@ -136,15 +137,37 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX) {
boxWidth += boxPadding * 2;
// Smart Positioning Logic
let boxX, connectorAnchorX;
let boxX;
let anchorOnRight = false;
if (mainMouseX > appState.p5_instance.width / 2) {
boxX = avgX - xOffset - boxWidth;
connectorAnchorX = boxX + boxWidth;
anchorOnRight = true;
} else {
boxX = avgX + xOffset;
connectorAnchorX = boxX;
anchorOnRight = false;
}
const boxY = avgY - boxHeight / 2;
let boxY = avgY - boxHeight / 2 - yOffset;
// --- START: Boundary Constraint Logic ---
// Calculate the visible bounds in the current coordinate system (which is scaled and translated)
const visibleW = p.width / zoomFactor;
const visibleH = p.height / zoomFactor;
// Use camera center instead of raw mouse position to accurately represent the visible viewport
const minVisX = smoothedCamX - visibleW / 2;
const maxVisX = smoothedCamX + visibleW / 2;
const minVisY = smoothedCamY - visibleH / 2;
const maxVisY = smoothedCamY + visibleH / 2;
const edgePad = 10 / zoomFactor;
// Constrain X & Y to keep tooltip within the zoom view
if (boxX + boxWidth > maxVisX - edgePad) boxX = maxVisX - edgePad - boxWidth;
if (boxX < minVisX + edgePad) boxX = minVisX + edgePad;
if (boxY + boxHeight > maxVisY - edgePad) boxY = maxVisY - edgePad - boxHeight;
if (boxY < minVisY + edgePad) boxY = minVisY + edgePad;
const connectorAnchorX = anchorOnRight ? boxX + boxWidth : boxX;
// --- END: Boundary Constraint Logic ---
// Draw highlighting circles
hoveredItems.forEach((item) => {
@ -190,10 +213,37 @@ export const zoomSketch = function (p) {
let canvas = null;
const containerId = "zoom-canvas-container";
// Persistent smoothed coordinates for the tooltip
let smoothedAvgX = null;
let smoothedAvgY = null;
// Smooth camera coordinates to prevent judder on high-refresh monitors (75Hz+)
let smoothedCamX = null;
let smoothedCamY = null;
appState.zoomFactor = 4; // Set a default zoom factor in the global state
appState.zoomLeadFactor = 0.2; // Control how much the circle "leads" the camera (0.0 = smooth, 1.0 = instant)
p.setup = function () {
p.noLoop();
// Optimization: Increase target frame rate.
// p5.js often defaults to 60fps. On 75Hz+ screens, this causes frame skipping and judder.
p.frameRate(144);
// We enable looping so the lerp smoothing can animate between frames
p.loop();
// --- START: ResizeObserver for ZoomSketch ---
let resizeDebounce = null;
const ro = new ResizeObserver(() => {
if (resizeDebounce) clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
if (canvas) {
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
}
}, 100);
});
const container = document.getElementById(containerId);
if(container) ro.observe(container);
// --- END: ResizeObserver for ZoomSketch ---
};
p.updateAndDraw = function (mainMouseX, mainMouseY, hoveredItems, scales) {
@ -206,23 +256,20 @@ export const zoomSketch = function (p) {
canvas.parent(containerId);
//console.log(`zoomSketch: Canvas CREATED with dimensions ${p.width}x${p.height}`); // debug
} else {
console.warn(
"zoomSketch: updateAndDraw called, but container is not ready. Aborting draw."
); //debug
return;
}
}
p.redraw();
// With loop() enabled, we don't strictly need redraw(),
// but it helps if updateAndDraw is called less frequently than the frame rate.
};
p.handleResize = function () {
console.log("zoomSketch: handleResize triggered. Destroying old canvas.");
if (canvas) {
canvas.remove(); // p5.js function to properly remove the canvas from the DOM
canvas = null; // Set the internal reference to null
p.windowResized = function () {}; // Disable native p5 window resize
p.handleContainerResize = function () {
const container = document.getElementById(containerId);
if (container && canvas) {
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
}
// The canvas will be recreated automatically the next time updateAndDraw() is called,
// at which point the container will have its correct, final dimensions.
};
p.draw = function () {
if (!appState.vizData || !canvas) return;
@ -234,22 +281,76 @@ export const zoomSketch = function (p) {
const { mainMouseX, mainMouseY, hoveredItems } = lastUpdate;
// --- Camera Smoothing (Prevents Judder) ---
// If the main app updates at 60Hz but this sketch runs at 75Hz, raw coordinates cause stutter.
if (smoothedCamX === null) {
smoothedCamX = mainMouseX;
smoothedCamY = mainMouseY;
}
const camSmoothing = 0.5;
const dt = Math.max(0, p.deltaTime);
const adjustedCamSmoothing = 1 - Math.pow(1 - camSmoothing, dt / (1000 / 60));
smoothedCamX = p.lerp(smoothedCamX, mainMouseX, adjustedCamSmoothing);
smoothedCamY = p.lerp(smoothedCamY, mainMouseY, adjustedCamSmoothing);
// --- Tooltip Smoothing (Low Pass Filter) ---
if (hoveredItems.length > 0) {
const targetAvgX = hoveredItems.reduce((acc, item) => acc + item.screenX, 0) / hoveredItems.length;
const targetAvgY = hoveredItems.reduce((acc, item) => acc + item.screenY, 0) / hoveredItems.length;
if (smoothedAvgX === null) {
smoothedAvgX = targetAvgX;
smoothedAvgY = targetAvgY;
} else {
// --- START: Frame-Rate Independent Smoothing ---
// We use p.deltaTime to adjust the smoothing factor so that the animation
// speed remains consistent across different monitor refresh rates.
const baseSmoothing = 0.05; // Target smoothing at 60 FPS
const dt = Math.max(0, p.deltaTime);
const adjustedSmoothing = 1 - Math.pow(1 - baseSmoothing, dt / (1000 / 60));
smoothedAvgX = p.lerp(smoothedAvgX, targetAvgX, adjustedSmoothing);
smoothedAvgY = p.lerp(smoothedAvgY, targetAvgY, adjustedSmoothing);
// --- END: Frame-Rate Independent Smoothing ---
}
} else {
smoothedAvgX = null;
smoothedAvgY = null;
}
p.push(); // Start zoom transformations
p.translate(
p.width / 2 - mainMouseX * appState.zoomFactor,
p.height / 2 - mainMouseY * appState.zoomFactor
p.width / 2 - smoothedCamX * appState.zoomFactor,
p.height / 2 - smoothedCamY * appState.zoomFactor
);
p.scale(appState.zoomFactor);
// --- Redraw the scene from scratch ---
if (appState.p5_instance && appState.p5_instance.getStaticBackground) {
p.image(
appState.p5_instance.getStaticBackground(),
0,
0,
appState.p5_instance.width,
appState.p5_instance.height
);
// Performance fix: Check if source has valid dimensions before drawing
if (appState.p5_instance && appState.p5_instance.width > 0 && appState.p5_instance.height > 0 && appState.p5_instance.getStaticBackground) {
const bg = appState.p5_instance.getStaticBackground();
// Optimization: Only draw the visible slice of the background
// Drawing the full 1920x1080 texture every frame is expensive if we only see a tiny part.
const imgW = bg.width;
const imgH = bg.height;
const visibleW = p.width / appState.zoomFactor;
const visibleH = p.height / appState.zoomFactor;
// Calculate World Coordinates of the top-left of the view
const sX = smoothedCamX - visibleW / 2;
const sY = smoothedCamY - visibleH / 2;
// Intersect visible view with image bounds
const dX = Math.max(0, sX);
const dY = Math.max(0, sY);
const dW = Math.min(imgW, sX + visibleW) - dX;
const dH = Math.min(imgH, sY + visibleH) - dY;
if (dW > 0 && dH > 0 && bg.width > 0 && bg.height > 0) {
// Draw only the visible sub-rectangle
// Since we are transformed to World Space, destination (dx,dy) matches source (dx,dy)
p.image(bg, dX, dY, dW, dH, dX, dY, dW, dH);
}
}
p.push(); // Start radar transformations
@ -260,17 +361,21 @@ export const zoomSketch = function (p) {
p.scale(1, -1);
const frameData = appState.vizData.radarFrames[appState.currentFrame];
drawAxes(p, plotScales);
drawEgoVehicle(p, plotScales);
const inverseZoom = 1 / appState.zoomFactor * 2;
// --- OPTIMIZATION: Axes and Ego Vehicle are already in the static background image ---
// drawAxes(p, plotScales);
// drawEgoVehicle(p, plotScales);
if (frameData) {
drawTrackMarkers(p, plotScales);
drawTrackMarkers(p, plotScales, inverseZoom, false);
drawRegionsOfInterest(p, frameData, plotScales);
if (toggleTracks.checked) {
drawTrajectories(p, plotScales);
drawTrajectories(p, plotScales, inverseZoom);
}
drawPointCloud(p, frameData.pointCloud, plotScales);
drawPointCloud(p, frameData.pointCloud, plotScales, 4 * inverseZoom);
if (toggleClusterColor.checked) {
drawClusterCentroids(p, frameData.clusters, plotScales);
drawClusterCentroids(p, frameData.clusters, plotScales, inverseZoom);
}
if (togglePredictedPos.checked) {
for (const track of appState.vizData.tracks) {
@ -285,12 +390,13 @@ export const zoomSketch = function (p) {
const pos = log.predictedPosition;
const x = pos[0] * plotScales.plotScaleX;
const y = pos[1] * plotScales.plotScaleY;
const size = 4 * inverseZoom;
p.push();
p.stroke(255, 0, 0); // Red for predicted
p.strokeWeight(2);
p.line(x - 4, y - 4, x + 4, y + 4);
p.line(x + 4, y - 4, x - 4, y + 4);
p.strokeWeight(2 * inverseZoom);
p.line(x - size, y - size, x + size, y + size);
p.line(x + size, y - size, x - size, y + size);
p.pop();
}
}
@ -298,8 +404,8 @@ export const zoomSketch = function (p) {
}
p.pop(); // End radar transformations
// --- Call the new, self-contained tooltip function ---
drawZoomTooltip(p, hoveredItems, mainMouseX);
// --- Call the new, self-contained tooltip function with smoothed coords ---
drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY, smoothedCamX, smoothedCamY);
// --- START: Draw Purple Debug Circle ---
// This circle represents the hover radius, drawn in the zoomed coordinate space.
@ -315,32 +421,48 @@ export const zoomSketch = function (p) {
p.strokeWeight(1 / appState.zoomFactor);
p.drawingContext.setLineDash([5 / appState.zoomFactor, 3 / appState.zoomFactor]);
// The circle is drawn at the mouse position from the main canvas.
p.ellipse(mainMouseX, mainMouseY, hoverRadius * 2, hoverRadius * 2);
// Control how much the circle "leads" the camera movement.
// 0.0 = Locked to center (smooth). 1.0 = Locked to mouse (jumpy/leads).
const leadFactor = appState.zoomLeadFactor;
const circleX = p.lerp(smoothedCamX, mainMouseX, leadFactor);
const circleY = p.lerp(smoothedCamY, mainMouseY, leadFactor);
p.ellipse(circleX, circleY, hoverRadius * 2, hoverRadius * 2);
p.drawingContext.setLineDash([]);
p.pop();
// --- END: Draw Purple Debug Circle ---
p.pop(); // End zoom transformations
// --- START: DRAW TITLE OVERLAY ---
// This code runs *after* the zoom transformations have been popped,
// so it draws directly onto the canvas as a fixed UI element.
// --- START: Draw Out of Bounds Overlay ---
if (appState.isMouseOutOfBounds && appState.isCloseUpMode) {
p.push();
const titleLabel = document.getElementById("toggle-close-up").parentElement;
const titleText = titleLabel ? titleLabel.textContent.trim() : "Zoom Mode";
const textColor = document.documentElement.classList.contains("dark")
? 220
: 80;
p.fill(textColor);
// Semi-transparent black background
p.fill(0, 0, 0, 150);
p.noStroke();
p.textSize(16);
p.textAlign(p.LEFT, p.TOP);
p.rectMode(p.CENTER);
p.rect(p.width / 2, p.height / 2, p.width, p.height);
// Draw the text
p.fill(255, 100, 100); // Red warning color
p.textAlign(p.CENTER, p.CENTER);
p.textSize(18);
p.textStyle(p.BOLD);
p.text(titleText, 10, 10);
p.pop();
// --- END: DRAW TITLE OVERLAY ---
// --- START: Draw Countdown Overlay ---
const yOffsetOffset = (appState.zoomCountdown !== null && appState.zoomCountdown > 0) ? 20 : 0;
p.text("Mouse pointer Out of Bounds", p.width / 2, p.height / 2 - yOffsetOffset);
if (appState.zoomCountdown !== null && appState.zoomCountdown > 0) {
p.fill(255);
p.textStyle(p.NORMAL);
p.text(`Closing in ${appState.zoomCountdown}...`, p.width / 2, p.height / 2 + 20);
}
p.pop();
}
// --- END: Draw Out of Bounds Overlay ---
// --- START: Draw Countdown Overlay ---
else if (appState.zoomCountdown !== null && appState.zoomCountdown > 0) {
p.push();
// Semi-transparent black background for readability
p.fill(0, 0, 0, 150);

131
steps/src/session.js

@ -0,0 +1,131 @@
import { appState } from "./state.js";
import { showModal } from "./modal.js";
import { loadFreshFileFromDB } from "./db.js";
import {
offsetInput,
speedSlider,
snrMinInput,
snrMaxInput,
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleStationaryColor,
toggleVelocity,
toggleTracks,
toggleEgoSpeed,
toggleFrameNorm,
toggleDebugOverlay,
toggleDebug2Overlay,
toggleCloseUp,
togglePredictedPos,
toggleCovariance,
toggleConfirmedOnly,
saveSessionBtn,
loadSessionBtn,
sessionFileInput,
} from "./dom.js";
function saveSession() {
if (!appState.jsonFilename && !appState.videoFilename) {
showModal("Nothing to save. Please load data files first.");
return;
}
const sessionState = {
version: 1,
jsonFilename: appState.jsonFilename,
videoFilename: appState.videoFilename,
offset: offsetInput.value,
playbackSpeed: speedSlider.value,
snrMin: snrMinInput.value,
snrMax: snrMaxInput.value,
toggles: {
snrColor: toggleSnrColor.checked,
clusterColor: toggleClusterColor.checked,
inlierColor: toggleInlierColor.checked,
stationaryColor: toggleStationaryColor.checked,
velocity: toggleVelocity.checked,
tracks: toggleTracks.checked,
egoSpeed: toggleEgoSpeed.checked,
frameNorm: toggleFrameNorm.checked,
debugOverlay: toggleDebugOverlay.checked,
debug2Overlay: toggleDebug2Overlay.checked,
closeUp: toggleCloseUp.checked,
predictedPos: togglePredictedPos.checked,
covariance: toggleCovariance.checked,
confirmedOnly: toggleConfirmedOnly.checked,
},
};
const sessionString = JSON.stringify(sessionState, null, 2);
const blob = new Blob([sessionString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const now = new Date();
const pad = (num) => String(num).padStart(2, "0");
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(
now.getDate()
)}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
const defaultFilename = `visualizer-session_${timestamp}.json`;
const a = document.createElement("a");
a.href = url;
a.download = defaultFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async function loadSession(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const sessionState = JSON.parse(e.target.result);
if (sessionState.version !== 1 || !sessionState.jsonFilename) {
showModal("Error: Invalid or corrupted session file.");
return;
}
const videoBlob = await loadFreshFileFromDB("video", sessionState.videoFilename);
const jsonBlob = await loadFreshFileFromDB("json", sessionState.jsonFilename);
if (!jsonBlob || (sessionState.videoFilename && !videoBlob)) {
showModal(`Session load failed: The required data files are not in the application's cache.
Please manually load '${sessionState.jsonFilename}' and '${sessionState.videoFilename}' before loading this session.`);
return;
}
localStorage.setItem("jsonFilename", sessionState.jsonFilename || "");
localStorage.setItem("videoFilename", sessionState.videoFilename || "");
localStorage.setItem("visualizerOffset", sessionState.offset || "0");
localStorage.setItem("playbackSpeed", sessionState.playbackSpeed || "1");
localStorage.setItem("snrMin", sessionState.snrMin || "");
localStorage.setItem("snrMax", sessionState.snrMax || "");
if (sessionState.toggles) {
localStorage.setItem("togglesState", JSON.stringify(sessionState.toggles));
}
showModal("Session files found in cache. The application will now reload.").then(() => {
window.location.reload();
});
} catch (error) {
showModal("Error: Could not parse the session file. It may be invalid.");
console.error("Session load error:", error);
}
};
reader.readAsText(file);
}
export function initSessionManagement() {
saveSessionBtn.addEventListener("click", saveSession);
loadSessionBtn.addEventListener("click", () => sessionFileInput.click());
sessionFileInput.addEventListener("change", (event) => {
loadSession(event.target.files[0]);
event.target.value = ""; // Clear the input for future loads.
});
}

23
steps/src/state.js

@ -1,14 +1,31 @@
import {
RADAR_X_MIN,
RADAR_X_MAX,
RADAR_Y_MIN,
RADAR_Y_MAX
} from "./constants.js";
export const appState = {
zoomHideDelayTimeout: null, // Timeout before the hide countdown begins
zoomCountdown: null, // Holds the number of seconds left before zoom hides
zoomCountdownInterval: null, // The interval timer for the countdown
fps: 0, // To store the calculated FPS for performance monitoring
isRawOnlyMode: false, // <-- ADD THIS LINE
videoReadyByFallback: false, // True if video resolved via loadedmetadata timeout
videoMissing: false, // True if user opts to continue without a video
isMouseOutOfBounds: false, // True when the mouse is outside the radar sketch canvas
zoomPanelExplicitlyClosed: false, // True if user clicked X on the zoom panel during zoom mode
gridStackInstance: null, // Holds reference to the main GridStack instance
// Stores the parsed visualization data (radar frames, tracks, etc.)
vizData: null,
zoomSketchInstance: null, // Add this line
// Stores the processed CAN bus data (speed, time)
offset: 0, // The calculated or manually set offset in milliseconds.
currentGraphScale: 10, // Current ms per block for the IFT graph (smooth zooming)
videoStartDate: null,
// The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename
radarStartTimeMs: 0,
@ -39,6 +56,7 @@ export const appState = {
lastFrameRenderTime: 0,
lastVideoFrameTime: 0,
videoFrameRenderTime: 0,
lastOverlayUpdateTime: 0, // Track time between overlay updates for smoothing
useCustomTtcScheme: false, // Flag to switch between default and custom
customTtcScheme: {
// Default values match the UI
@ -53,4 +71,9 @@ export const appState = {
consecutiveResyncs: 0, // Counter for consecutive resyncs
isInLockdown: false, // Flag to prevent nested lockdown triggers
// --- Dynamic Radar Boundaries ---
radarXMin: RADAR_X_MIN,
radarXMax: RADAR_X_MAX,
radarYMin: RADAR_Y_MIN,
radarYMax: RADAR_Y_MAX,
};

438
steps/src/sync.js

@ -1,7 +1,6 @@
import { appState } from "./state.js";
import {
timelineSlider,
speedSlider,
offsetInput,
stopBtn,
playPauseBtn,
@ -9,35 +8,45 @@ import {
updatePersistentOverlays,
videoPlayer,
frameCounter,
canvasContainer,
toggleEgoSpeed,
egoSpeedDisplay,
canSpeedDisplay,
autoOffsetIndicator,
speedGraphContainer
} from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js";
import { throttledUpdateExplorer } from "./dataExplorer.js";
import { VIDEO_FPS } from "./constants.js";
import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js";
import { throttledUpdateExplorer, isExplorerOpen } from "./dataExplorer.js";
import { debugFlags } from "./debug.js";
import { saveManualOffset } from "./db.js";
// --- [START] MOVED FROM DOM.JS ---
//----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state.
export function resetVisualization() {
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
if (appState.vizData) {
const numFrames = appState.vizData.radarFrames.length;
timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0;
updateFrame(0, true); // Update to the first frame and force video seek
} else {
timelineSlider.max = 0;
timelineSlider.value = 0;
if (videoPlayer.src) {
videoPlayer.currentTime = 0;
}
}
}
// --- NEW Playback Control Functions ---
let seekDebounceTimer = null;
let lastScrollTime = 0;
let scrollSpeed = 0;
export function startPlayback() {
if (videoPlayer.src && videoPlayer.readyState > 1) {
appState.masterClockStart = performance.now();
appState.mediaTimeStart = videoPlayer.currentTime;
appState.lastSyncTime = appState.masterClockStart;
videoPlayer.play();
videoPlayer.requestVideoFrameCallback(videoFrameCallback); // Start the high-precision loop
}
@ -50,13 +59,27 @@ export function pausePlayback() {
}
}
export function forceResyncWithOffset() {
export function forceResyncWithOffset(saveToDb = true) {
// Make sure visualization data is loaded before proceeding
if (!appState.vizData) return;
console.log(
`Forcing resync with new offset: ${offsetInput.value}`
);
const newOffset = parseFloat(offsetInput.value) || 0;
appState.offset = newOffset; // Update the central state
// Persist the manual offset to IndexedDB for this specific file
if (saveToDb && appState.jsonFilename) {
saveManualOffset(appState.jsonFilename, newOffset);
}
// Re-Bake: Overwrite the pre-calculated sync times with the new offset.
precomputeRadarVideoSync(appState.vizData, appState.offset);
// --- START: Manual Offset UI Update ---
// When the user manually sets an offset, we need to update the UI immediately.
autoOffsetIndicator.textContent = "Manual"; // Set text
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-gray-500"; // Use consistent gray styling
// --- END: Manual Offset UI Update ---
console.log(`Forcing resync with new offset: ${appState.offset}ms`);
// If the video is playing, pause it to allow for precise frame tuning.
if (appState.isPlaying) {
@ -73,7 +96,7 @@ export function forceResyncWithOffset() {
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.
export function updateFrame(frame, forceVideoSeek) {
export function updateFrame(frame, forceVideoSeek = false, overrideTime = null) {
const startTime = performance.now(); //start emasuring timer of performance.
if (
!appState.vizData ||
@ -84,18 +107,30 @@ export function updateFrame(frame, forceVideoSeek) {
return; // Exit if no visualization data or invalid frame
appState.currentFrame = frame;
timelineSlider.value = appState.currentFrame;
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${
appState.vizData.radarFrames.length
}`;
// --- Optimization: Guarded Text Updates ---
const newFrameText = `Frame: ${appState.currentFrame + 1} / ${appState.vizData.radarFrames.length}`;
if (frameCounter.textContent !== newFrameText) {
frameCounter.textContent = newFrameText;
}
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (toggleEgoSpeed.checked && frameData) {
// Update ego speed display if enabled.
const egoVy_kmh = (frameData.egoVelocity[1] * 3.6).toFixed(1); // Convert m/s to km/h and format
egoSpeedDisplay.textContent = `Ego: ${egoVy_kmh} km/h`;
const newEgoText = `Ego: ${egoVy_kmh} km/h`;
if (egoSpeedDisplay.textContent !== newEgoText) {
egoSpeedDisplay.textContent = newEgoText;
}
if (egoSpeedDisplay.classList.contains("hidden")) {
egoSpeedDisplay.classList.remove("hidden");
}
} else {
if (!egoSpeedDisplay.classList.contains("hidden")) {
egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display.
}
}
// --- ADD THIS NEW BLOCK ---
if (
@ -103,53 +138,58 @@ export function updateFrame(frame, forceVideoSeek) {
frameData.canVehSpeed_kmph !== null &&
!isNaN(frameData.canVehSpeed_kmph)
) {
canSpeedDisplay.textContent = `CAN: ${frameData.canVehSpeed_kmph.toFixed(
1
)} km/h`;
const newCanText = `CAN: ${frameData.canVehSpeed_kmph.toFixed(1)} km/h`;
if (canSpeedDisplay.textContent !== newCanText) {
canSpeedDisplay.textContent = newCanText;
}
if (canSpeedDisplay.classList.contains("hidden")) {
canSpeedDisplay.classList.remove("hidden");
}
} else {
if (!canSpeedDisplay.classList.contains("hidden")) {
canSpeedDisplay.classList.add("hidden");
}
}
// --- END OF NEW BLOCK ---
let timeForUpdates = videoPlayer.currentTime; // NEW: Default to the video's current time
if (
forceVideoSeek &&
videoPlayer.src &&
videoPlayer.readyState > 1 &&
appState.videoStartDate &&
frameData
) {
const offsetMs = parseFloat(offsetInput.value) || 0;
const targetRadarTimeMs = frameData.timestampMs;
const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000;
if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) {
// Convert frame's relative time to the video's timeline
const targetVideoTimeSec = frameData.videoSyncedTime;
if (targetVideoTimeSec >= 0 && videoPlayer.duration && targetVideoTimeSec <= videoPlayer.duration) {
// Ensure target time is within video duration
if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) {
// Check for significant drift
videoPlayer.currentTime = targetVideoTimeSec; // Seek video if drift is significant
}
// MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime
timeForUpdates = targetVideoTimeSec; // Update time for subsequent UI updates
}
} // End of forceVideoSeek block
if (!appState.isPlaying) {
// MODIFIED: Use our new synchronized time variable
updatePersistentOverlays(timeForUpdates);
}
// --- End of fix ---
// --- START: Conditional Redraw Logic ---
// Only force a redraw from here if the animation loop is NOT running.
// When playing, the animationLoop is responsible for redrawing.
if (!appState.isPlaying && appState.p5_instance) appState.p5_instance.redraw();
if (!appState.isPlaying && appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// The animationLoop is now responsible for all redraws.
// We no longer call redraw() from here.
// --- NEW: Centralized Explorer Update ---
if (isExplorerOpen) {
throttledUpdateExplorer();
}
// --- END: Centralized Explorer Update ---
const endTime = performance.now();
appState.lastFrameRenderTime = endTime - startTime; // <-- End timer and update state
// --- START: FIX for Overlay Visibility During Scrubbing ---
// Update overlays here to ensure they refresh when scrubbing while paused.
// If an overrideTime is provided (e.g., from a scroll-seek), use it.
// Otherwise, use the video player's current time.
const displayTime = overrideTime !== null ? overrideTime : videoPlayer.currentTime;
updatePersistentOverlays(displayTime);
updateDebugOverlay(displayTime);
// --- END: FIX for Overlay Visibility During Scrubbing ---
}
// --- [END] MOVED FROM DOM.JS ---
@ -162,114 +202,46 @@ export function stopPlayback() {
}
}
/**
* DATA LOOP: Runs on the video's clock (~30 FPS).
* Its ONLY job is to update appState.currentFrame. It does NO drawing.
*/
export function videoFrameCallback(now, metadata) {
// If the video is no longer playing, stop the callback loop.
if (!appState.isPlaying || videoPlayer.paused) {
if (debugFlags.sync) {
console.log(`[${performance.now().toFixed(3)}] vfc_DEBUG: videoFrameCallback running.`);
}
if (!appState.isPlaying || videoPlayer.paused || !appState.vizData) {
return;
}
// This is now the main animation driver during playback.
// It's perfectly synced with the video's frame presentation.
const currentTime = metadata.mediaTime;
const frameIndex = findRadarFrameIndexForTime(currentTime * 1000);
// 1. Get the video's current time directly from the callback metadata.
const videoCurrentTime = metadata.mediaTime;
updateFrame(frameIndex, false); // Update radar, but don't seek video.
// 2. Find the corresponding radar frame index.
const frameIndex = findRadarFrameIndexForTime(videoCurrentTime, appState.vizData);
// 3. Update the application state if the frame has changed.
if (frameIndex !== appState.currentFrame) {
appState.currentFrame = frameIndex;
// This is the ONLY state this function should change. All UI updates are in animationLoop.
}
// Re-register the callback for the next frame to create a loop
videoPlayer.requestVideoFrameCallback(videoFrameCallback);
}
/**
* RENDER LOOP: Runs on the monitor's refresh rate (~60+ FPS).
* Its ONLY job is to draw the current state. It does NO data calculation.
*/
export function animationLoop() {
if (!appState.isPlaying) return;
// Get the current playback speed from the slider
const playbackSpeed = parseFloat(speedSlider.value);
// Calculate the elapsed real time since the master clock started
const elapsedRealTime = performance.now() - appState.masterClockStart;
// Calculate the current media time based on the master clock, initial media time, elapsed real time, and playback speed
const currentMediaTime =
appState.mediaTimeStart + (elapsedRealTime / 1000) * playbackSpeed;
// Update radar frame based on the master clock
// Check if visualization data and video start date are available
if (appState.vizData && appState.videoStartDate) {
// Get the offset from the input field, default to 0 if not a valid number
// --- START: Corrected Logic ---
const offsetMs = parseFloat(offsetInput.value) || 0;
// The master clock represents the VIDEO's timeline.
// To find the corresponding RADAR time, we must add the offset.
const targetRadarTimeMs = currentMediaTime * 1000 + offsetMs;
const targetFrame = findRadarFrameIndexForTime(
targetRadarTimeMs,
appState.vizData
);
// --- END: Corrected Logic ---
if (targetFrame !== appState.currentFrame) {
// Update the displayed frame if it's different from the current one
updateFrame(targetFrame, false);
}
}
// Periodically check for drift between master clock and video element
const now = performance.now();
if (now - appState.lastSyncTime > 150) {
const videoTime = videoPlayer.currentTime;
const drift = Math.abs(currentMediaTime - videoTime);
// Resync if drift is > 200ms
if (drift > 0.2) { // The drift threshold is 200ms.
console.warn(`Resyncing video. Drift was: ${drift.toFixed(3)}s`);
// --- START: Resync Storm "Circuit Breaker" ---
if (appState.isResyncLockdownEnabled) {
const now = performance.now();
// If the last resync was recent (within 2s), increment the counter. Otherwise, reset it.
if (appState.lastResyncTimestamp && (now - appState.lastResyncTimestamp < 2000)) {
appState.consecutiveResyncs = (appState.consecutiveResyncs || 0) + 1;
} else {
appState.consecutiveResyncs = 1;
}
appState.lastResyncTimestamp = now;
// If more than 2 consecutive resyncs have occurred, trigger the lockdown.
if (appState.consecutiveResyncs > 2) {
// --- START: FIX for Lockdown Loop ---
if (!appState.isInLockdown) { // Only trigger if not already in lockdown
console.warn("Resync storm detected! Pausing playback to recover...");
appState.isInLockdown = true; // Enter lockdown state
pausePlayback(); // Pause the video.
// After a 1-second pause, resume playback.
// startPlayback() will handle the clock reset automatically.
setTimeout(() => {
console.log("Resuming playback after lockdown.");
appState.isInLockdown = false; // Exit lockdown state
startPlayback(); // Resume playback, which now correctly resets the clock.
}, 1000); // 1-second pause.
appState.consecutiveResyncs = 0; // Reset the counter.
return; // Exit the animation loop for this frame to allow the pause.
}
// --- END: FIX for Lockdown Loop ---
}
}
// --- END: Resync Storm "Circuit Breaker" ---
videoPlayer.currentTime = currentMediaTime; // Perform the standard resync.
}
appState.lastSyncTime = now;
}
// Stop playback at the end of the video
if (currentMediaTime >= videoPlayer.duration) {
stopBtn.click();
return;
if (debugFlags.sync) {
console.log(`[${performance.now().toFixed(3)}] anim_DEBUG: animationLoop running.`);
}
// Update debug overlay information
updatePersistentOverlays(currentMediaTime);
updateDebugOverlay(currentMediaTime);
// The render loop is responsible for ALL UI updates, ensuring perfect sync.
updateFrame(appState.currentFrame);
// --- START: Centralized Redraw Logic ---
// Explicitly redraw all active sketches in sync with the animation frame.
@ -278,92 +250,178 @@ export function animationLoop() {
// --- END: Centralized Redraw Logic ---
// Request the next frame
if (appState.isPlaying) {
requestAnimationFrame(animationLoop);
}
}
let timelineDebounceTimer;
export function handleTimelineInput(event) {
if (!appState.vizData) return;
updateDebugOverlay(videoPlayer.currentTime);
updatePersistentOverlays(videoPlayer.currentTime);
// --- 1. Live Seeking (Throttled for performance) ---
// This part gives you the immediate visual feedback as you drag the slider.
// We use a simple timestamp check to prevent it from running too often.
const now = performance.now();
if (
!timelineSlider.lastInputTime ||
now - timelineSlider.lastInputTime > 32
) {
// ~30fps throttle
// 1. If playing, pause playback to allow scrubbing.
if (appState.isPlaying) {
videoPlayer.pause();
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
const frame = parseInt(event.target.value, 10);
updateFrame(frame, true);
appState.mediaTimeStart = videoPlayer.currentTime;
appState.masterClockStart = now;
}
// --- 2. Final, Precise Sync (Debounced for reliability) ---
// This part ensures a perfect sync only AFTER you stop moving the slider.
clearTimeout(seekDebounceTimer); // Always cancel the previously scheduled sync
// 2. Get the target frame from the slider.
const frame = parseInt(event.target.value, 10);
seekDebounceTimer = setTimeout(() => {
console.log("Slider movement stopped. Performing final, debounced resync.");
const finalFrame = parseInt(event.target.value, 10);
updateFrame(finalFrame, true); // Perform the final, precise seek
// 3. Update UI immediately for responsiveness, but WITHOUT forcing a video seek.
updateFrame(frame, false);
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// Also update the debug overlay with the final, settled time
updateDebugOverlay(videoPlayer.currentTime);
}, 250); // Wait for 250ms of inactivity before firing
// 4. Use a debouncer to perform the expensive video seek after the user stops dragging.
clearTimeout(timelineDebounceTimer);
timelineDebounceTimer = setTimeout(() => {
updateFrame(appState.currentFrame, true); // Perform final, precise video seek.
}, 300); // 300ms delay after last input event.
}
export function handleTimelineWheel(event) {
if (!appState.vizData) return;
// 1. Prevent the page from scrolling up and down
event.preventDefault();
let lastScrollTime = 0;
let scrollSpeed = 0;
let seekDebounceTimer;
let lastVideoScrollTime = 0;
let videoScrollSpeed = 0;
let videoSeekDebounceTimer;
let targetVideoTime = null; // NEW: State variable to track target time during scroll
function handleTimelineWheel(event) {
// If no data, or if close-up mode is active, do not seek.
// The wheel event is used for zooming in close-up mode, unless Shift is held.
if (!appState.vizData || (appState.isCloseUpMode && !event.shiftKey)) {
return;
}
event.preventDefault(); // Prevent default page scroll
// 1. Pause playback if the user starts scrubbing.
if (appState.isPlaying) {
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
// 2. Calculate scroll speed
// 2. Calculate scroll speed to create a dynamic seek amount.
const now = performance.now();
const timeDelta = now - (lastScrollTime || now); // Handle first scroll
const timeDelta = now - (lastScrollTime || now);
lastScrollTime = now;
// Calculate speed as "events per second", giving more weight to recent, fast scrolls
scrollSpeed = timeDelta > 0 ? 1000 / timeDelta : scrollSpeed;
// 3. Map scroll speed to a dynamic seek multiplier
// This creates a nice acceleration curve. The '50' is a sensitivity value you can adjust.
// 3. Map scroll speed to an acceleration curve.
// The sensitivity value (e.g., 4) can be adjusted for more/less acceleration.
const speedMultiplier = 1 + Math.floor(scrollSpeed / 4);
const baseSeekAmount = 1; // Base frames to move on a slow scroll
let seekAmount = Math.max(baseSeekAmount, speedMultiplier);
const seekAmount = Math.max(1, speedMultiplier); // Ensure we always move at least 1 frame.
// 4. Calculate the new frame index
const direction = Math.sign(event.deltaY); // +1 for down/right, -1 for up/left
const currentFrame = parseInt(timelineSlider.value, 10);
let newFrame = currentFrame - seekAmount * direction;
// 4. Calculate the new frame index.
const direction = Math.sign(event.deltaY);
// Scrolling down (positive deltaY) should advance the frame (increase index).
let newFrame = appState.currentFrame + direction * seekAmount;
// Clamp the new frame to the valid range
// 5. Clamp the new frame to the valid range.
const totalFrames = appState.vizData.radarFrames.length - 1;
newFrame = Math.max(0, Math.min(newFrame, totalFrames));
// 5. Update the UI
if (appState.isPlaying) {
playPauseBtn.click(); // Pause if playing
}
updateFrame(newFrame, true);
// 6. Update the UI immediately for responsive feedback, but WITHOUT forcing a video seek.
// This makes the slider feel fast without causing video stutter.
updateFrame(newFrame, false);
// --- START: Immediate Redraw for Responsiveness ---
// Manually trigger redraws here so the radar visualization updates as the user scrolls.
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// --- END: Immediate Redraw for Responsiveness ---
// 6. Reuse the debouncer for a final, precise sync after scrolling stops
// 7. Use a debouncer for the expensive video seek. This will only run once
// after the user has finished scrolling, ensuring a final, precise sync.
clearTimeout(seekDebounceTimer);
seekDebounceTimer = setTimeout(() => {
console.log("Scrolling stopped. Performing final, debounced resync.");
updateFrame(newFrame, true);
updatePersistentOverlays(videoPlayer.currentTime);
updateDebugOverlay(videoPlayer.currentTime);
}, 300); // Wait 300ms after the last scroll event
throttledUpdateExplorer();
// Perform the final, expensive video seek.
updateFrame(appState.currentFrame, true);
}, 300); // 300ms delay after the last scroll event.
}
function handleVideoPanelWheel(event) {
if (!appState.vizData || !videoPlayer.src || videoPlayer.duration <= 0) return;
event.preventDefault(); // Prevent default page scroll
// 1. On the first scroll event, pause playback and initialize our target time.
if (appState.isPlaying) {
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
if (targetVideoTime === null) {
targetVideoTime = videoPlayer.currentTime;
}
// 2. Calculate scroll speed for acceleration.
const now = performance.now();
const timeDelta = now - (lastVideoScrollTime || now);
lastVideoScrollTime = now;
videoScrollSpeed = timeDelta > 0 ? 1000 / timeDelta : videoScrollSpeed;
// 3. Map scroll speed to an acceleration curve.
const speedMultiplier = Math.floor(videoScrollSpeed / 8);
const seekAmount = Math.max(1, speedMultiplier); // Always move at least 1 frame.
// 4. Calculate the new target time based on our stateful variable.
const direction = Math.sign(event.deltaY);
const timeIncrement = (direction * seekAmount) / VIDEO_FPS;
targetVideoTime += timeIncrement;
// 5. Clamp the new time to the video's bounds.
targetVideoTime = Math.max(0, Math.min(targetVideoTime, videoPlayer.duration));
// 6. Find the corresponding radar frame for the new target time.
const newRadarFrame = findRadarFrameIndexForTime(targetVideoTime, appState.vizData);
console.log('--- Video Wheel Debug ---');
console.log(`Scroll Speed: ${videoScrollSpeed.toFixed(2)}`);
console.log(`Seek Amount (frames): ${seekAmount}`);
console.log(`Time Increment (s): ${timeIncrement.toFixed(4)}`);
console.log(`New Target Time (s): ${targetVideoTime.toFixed(4)}`);
console.log(`New Radar Frame: ${newRadarFrame}`);
// 7. Update the UI immediately for responsive feedback, but WITHOUT forcing a video seek.
updateFrame(newRadarFrame, false, targetVideoTime);
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// 8. Use a debouncer for the expensive video seek.
clearTimeout(videoSeekDebounceTimer);
videoSeekDebounceTimer = setTimeout(() => {
console.log(`--- Debounced Seek Fired ---`);
console.log(`Final Seek Time (s): ${targetVideoTime.toFixed(4)}`);
// Perform the final, expensive video seek.
videoPlayer.currentTime = targetVideoTime;
// Reset the state variable, so the next scroll interaction starts fresh.
targetVideoTime = null;
lastVideoScrollTime = 0; // Also reset scroll time to prevent huge initial jump
videoScrollSpeed = 0; // FIX: Reset scroll speed to prevent "sticky" acceleration.
}, 150);
}
export function initSyncUIHandlers() {
timelineSlider.addEventListener("input", handleTimelineInput);
timelineSlider.addEventListener("wheel", handleTimelineWheel);
timelineSlider.addEventListener("wheel", handleTimelineWheel, {
passive: false,
});
// Use the canvas container for radar frame seeking
canvasContainer.addEventListener("wheel", handleTimelineWheel, {
passive: false,
});
// Use the video player for video frame seeking
videoPlayer.addEventListener("wheel", handleVideoPanelWheel, {
passive: false,
});
// Use the speed graph container for radar frame seeking
speedGraphContainer.addEventListener("wheel", handleTimelineWheel, {
passive: false,
});
}

36
steps/src/theme.js

@ -1,5 +1,5 @@
import { appState } from "./state.js";
import { videoPlayer, themeToggleBtn} from "./dom.js";
import { videoPlayer, themeToggleBtn, startThemeToggleBtn, startThemeToggleDarkIcon, startThemeToggleLightIcon } from "./dom.js";
const darkIcon = document.getElementById("theme-toggle-dark-icon");
const lightIcon = document.getElementById("theme-toggle-light-icon");
@ -8,11 +8,15 @@ function setTheme(theme) {
document.documentElement.classList.add("dark");
lightIcon.classList.remove("hidden");
darkIcon.classList.add("hidden");
if (startThemeToggleLightIcon) startThemeToggleLightIcon.classList.remove("hidden");
if (startThemeToggleDarkIcon) startThemeToggleDarkIcon.classList.add("hidden");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
darkIcon.classList.remove("hidden");
lightIcon.classList.add("hidden");
if (startThemeToggleDarkIcon) startThemeToggleDarkIcon.classList.remove("hidden");
if (startThemeToggleLightIcon) startThemeToggleLightIcon.classList.add("hidden");
localStorage.setItem("color-theme", "light");
}
@ -27,17 +31,23 @@ function setTheme(theme) {
// Redraw the speed graph to apply theme changes
if (appState.speedGraphInstance) {
// Check if there's data available to redraw
if (appState.vizData && videoPlayer.duration) {
// Re-run setData. This is the most reliable way to redraw the graph
// with the new theme, as it recalculates and redraws everything.
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
// Redraw the static background buffer with the new theme colors and then redraw the canvas.
// This avoids calling setData, which can have unintended side effects.
if (appState.vizData) {
appState.speedGraphInstance.drawStaticGraphToBuffer(appState.vizData);
appState.speedGraphInstance.redraw();
}
}
// Notify Iframes about theme change
const manualIframe = document.getElementById('user-manual-iframe');
if (manualIframe && manualIframe.contentWindow) {
manualIframe.contentWindow.postMessage({ type: 'theme-change', theme: theme }, '*');
}
const codebaseIframe = document.getElementById('codebase-iframe');
if (codebaseIframe && codebaseIframe.contentWindow) {
codebaseIframe.contentWindow.postMessage({ type: 'theme-change', theme: theme }, '*');
}
}
export function initializeTheme() {
@ -56,4 +66,12 @@ export function initializeTheme() {
setTheme("dark");
}
});
startThemeToggleBtn.addEventListener("click", () => {
if (document.documentElement.classList.contains("dark")) {
setTheme("light");
} else {
setTheme("dark");
}
});
}

532
steps/src/ui.js

@ -0,0 +1,532 @@
import { appState } from "./state.js";
import { formatTime } from "./utils.js";
import { showModal } from "./modal.js";
import { pausePlayback } from "./sync.js";
import {
videoPlayer,
timelineSlider,
speedSlider,
speedDisplay,
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleStationaryColor,
toggleVelocity,
toggleEgoSpeed,
toggleFrameNorm,
toggleTracks,
toggleDebugOverlay,
toggleDebug2Overlay,
toggleCloseUp,
toggleCovariance,
toggleVehicleDimensions,
snrMinInput,
snrMaxInput,
applySnrBtn,
timelineTooltip,
playPauseBtn,
updatePersistentOverlays,
updateDebugOverlay,
collapsibleMenu,
toggleMenuBtn,
closeMenuBtn,
menuScrim,
fullscreenBtn,
toggleConfirmedOnly,
shortcutsBtn,
shortcutsModal,
shortcutsModalCloseBtn,
userManualBtn,
guideModal,
guideModalCloseBtn,
codebaseBtn,
codebaseModal,
codebaseModalCloseBtn,
changelogBtn,
changelogModal,
changelogModalCloseBtn,
startUserManualBtn,
startCodebaseBtn,
startChangelogBtn,
} from "./dom.js";
// --- START: Resizable and Draggable Panel Logic ---
export function makeDraggableAndResizable(panel, header, minWidth = 400, minHeight = 300) {
if (!panel || !header) return;
const resizers = panel.querySelectorAll('.resizer');
let original_width = 0;
let original_height = 0;
let original_x = 0;
let original_y = 0;
let original_mouse_x = 0;
let original_mouse_y = 0;
// --- Persistence Logic ---
const storageKey = `panel_pos_${panel.id}`;
function savePosition() {
if (!panel.id) return;
const state = {
left: panel.style.left,
top: panel.style.top,
width: panel.style.width,
height: panel.style.height
};
console.log(`Saving position for ${panel.id}`, state);
localStorage.setItem(storageKey, JSON.stringify(state));
}
function loadPosition() {
if (!panel.id) return;
const saved = localStorage.getItem(storageKey);
if (saved) {
try {
const state = JSON.parse(saved);
console.log(`Loading position for ${panel.id}`, state);
if (state.left) panel.style.left = state.left;
if (state.top) panel.style.top = state.top;
if (state.width) panel.style.width = state.width;
if (state.height) panel.style.height = state.height;
// Ensure it's still in view
requestAnimationFrame(() => constrainToViewport());
} catch (e) { console.error(`Failed to load position for ${panel.id}`, e); }
} else {
console.log(`No saved position found for ${panel.id}`);
}
}
// --- Auto-Focus (Bring to Front) ---
panel.addEventListener('mousedown', () => {
document.querySelectorAll('#zoom-panel, #data-explorer-panel').forEach(p => {
p.style.zIndex = "30";
});
panel.style.zIndex = "40";
});
loadPosition();
// --- Dragging Logic ---
header.addEventListener('mousedown', (e) => {
// Prevent drag if clicking buttons
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return;
e.preventDefault();
// Ensure panel floats on top
panel.style.zIndex = 100;
original_x = panel.offsetLeft;
original_y = panel.offsetTop;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
document.body.classList.add('dragging');
window.addEventListener('mousemove', dragPanel);
window.addEventListener('mouseup', stopDrag);
});
function dragPanel(e) {
const dx = e.pageX - original_mouse_x;
const dy = e.pageY - original_mouse_y;
panel.style.left = `${original_x + dx}px`;
panel.style.top = `${original_y + dy}px`;
}
function stopDrag() {
document.body.classList.remove('dragging');
window.removeEventListener('mousemove', dragPanel);
window.removeEventListener('mouseup', stopDrag);
savePosition();
}
// --- Resizing Logic ---
resizers.forEach(resizer => {
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
panel.style.zIndex = 100;
original_width = parseFloat(getComputedStyle(panel, null).getPropertyValue('width').replace('px', ''));
original_height = parseFloat(getComputedStyle(panel, null).getPropertyValue('height').replace('px', ''));
original_x = panel.getBoundingClientRect().left;
original_y = panel.getBoundingClientRect().top;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
const resizeFunc = (event) => resizePanel(event, resizer.classList);
document.body.classList.add('resizing');
window.addEventListener('mousemove', resizeFunc);
window.addEventListener('mouseup', () => {
document.body.classList.remove('resizing');
window.removeEventListener('mousemove', resizeFunc);
savePosition();
if (panel.id === 'zoom-panel' && appState.zoomSketchInstance) {
appState.zoomSketchInstance.handleContainerResize();
}
});
});
});
function resizePanel(e, direction) {
if (direction.toString().includes('r')) {
const width = original_width + (e.pageX - original_mouse_x);
if (width > minWidth) panel.style.width = `${width}px`;
}
if (direction.toString().includes('b')) {
const height = original_height + (e.pageY - original_mouse_y);
if (height > minHeight) panel.style.height = `${height}px`;
}
if (direction.toString().includes('l')) {
const newWidth = original_width - (e.pageX - original_mouse_x);
if (newWidth > minWidth) {
panel.style.width = `${newWidth}px`;
panel.style.left = `${original_x + (e.pageX - original_mouse_x)}px`;
}
}
if (direction.toString().includes('t')) {
const newHeight = original_height - (e.pageY - original_mouse_y);
if (newHeight > minHeight) {
panel.style.height = `${newHeight}px`;
panel.style.top = `${original_y + (e.pageY - original_mouse_y)}px`;
}
}
}
// --- Viewport Constraint Logic ---
// This ensures that if the user resizes their browser, the panel doesn't get "lost" off-screen.
function constrainToViewport() {
const rect = panel.getBoundingClientRect();
const margin = 10; // Extra padding
// Horizontal constraint
if (rect.right > window.innerWidth) {
panel.style.left = `${Math.max(margin, window.innerWidth - rect.width - margin)}px`;
}
if (rect.left < 0) {
panel.style.left = `${margin}px`;
}
// Vertical constraint
if (rect.bottom > window.innerHeight) {
panel.style.top = `${Math.max(margin, window.innerHeight - rect.height - margin)}px`;
}
if (rect.top < 0) {
panel.style.top = `${margin}px`;
}
}
window.addEventListener('resize', constrainToViewport);
}
// --- END: Resizable and Draggable Panel Logic ---
function toggleMenu(show) {
if (show) {
collapsibleMenu.classList.remove("-translate-x-full");
menuScrim.classList.remove("hidden");
} else {
collapsibleMenu.classList.add("-translate-x-full");
menuScrim.classList.add("hidden");
}
}
function toggleShortcutsModal(show) {
if (show) {
shortcutsModal.classList.remove("hidden");
} else {
shortcutsModal.classList.add("hidden");
}
}
function toggleGuideModal(show) {
if (show) {
guideModal.classList.remove("hidden");
} else {
guideModal.classList.add("hidden");
}
}
function toggleCodebaseModal(show) {
if (show) {
codebaseModal.classList.remove("hidden");
// Reset iframe to ensure it starts at the top
const iframe = codebaseModal.querySelector("iframe");
if (iframe) {
iframe.src = iframe.src;
}
} else {
codebaseModal.classList.add("hidden");
}
}
function toggleChangelogModal(show) {
if (show) {
changelogModal.classList.remove("hidden");
// Reset iframe to ensure it starts at the top
const iframe = changelogModal.querySelector("iframe");
if (iframe) {
iframe.src = iframe.src;
}
} else {
changelogModal.classList.add("hidden");
}
}
function handleColorToggles(e) {
const colorToggles = [
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleStationaryColor,
];
if (e.target.checked) {
colorToggles.forEach((o) => {
if (o !== e.target) o.checked = false;
});
}
if (appState.p5_instance) appState.p5_instance.redraw();
updatePersistentOverlays(videoPlayer.currentTime);
}
export function initUIEventListeners() {
// --- Initialize GridStack ---
if (typeof GridStack !== 'undefined') {
appState.gridStackInstance = GridStack.init({
margin: 10,
cellHeight: '6vh',
disableOneColumnMode: true,
animate: true,
handle: '.grid-stack-item-content > .cursor-grab',
});
// Load saved layout with a small delay to ensure DOM is ready
let isInitialLoad = true;
setTimeout(() => {
const savedLayout = localStorage.getItem('gridstack_layout');
if (savedLayout) {
try {
const layout = JSON.parse(savedLayout);
console.log("Restoring GridStack positions", layout);
// Use "soft load" to updates positions by id without replacing DOM
layout.forEach(item => {
const id = item.id || item.gsId;
if (id) {
const el = document.querySelector(`.grid-stack-item[gs-id="${id}"]`);
if (el) appState.gridStackInstance.update(el, { x: item.x, y: item.y, w: item.w, h: item.h });
}
});
} catch (e) { }
}
isInitialLoad = false;
}, 100);
// Save layout on changes
const saveGrid = () => {
if (isInitialLoad) return; // Don't save while loading
// save(true, false) saves all items with their current positions/sizes
const layout = appState.gridStackInstance.save(true, false);
console.log("Saving GridStack layout", layout);
localStorage.setItem('gridstack_layout', JSON.stringify(layout));
};
appState.gridStackInstance.on('change', saveGrid);
appState.gridStackInstance.on('dragstop', saveGrid);
appState.gridStackInstance.on('resizestop', saveGrid);
}
// --- Initialize Floating Zoom Panel ---
const zoomPanel = document.getElementById("zoom-panel");
const zoomHeader = document.getElementById("zoom-panel-header");
const closeZoomBtn = document.getElementById("close-zoom-btn");
if (zoomPanel && zoomHeader) {
makeDraggableAndResizable(zoomPanel, zoomHeader, 300, 200);
if (closeZoomBtn) {
closeZoomBtn.addEventListener("click", () => {
zoomPanel.classList.add("hidden");
appState.zoomPanelExplicitlyClosed = true;
});
}
}
// --- Shortcuts Modal ---
shortcutsBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleShortcutsModal(true);
});
shortcutsModalCloseBtn.addEventListener("click", () => toggleShortcutsModal(false));
shortcutsModal.addEventListener("click", (e) => {
// Close if clicking the background overlay (self), but not children
if (e.target === shortcutsModal) {
toggleShortcutsModal(false);
}
});
// --- Guide Modal ---
userManualBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleGuideModal(true);
});
startUserManualBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleGuideModal(true);
});
guideModalCloseBtn.addEventListener("click", () => toggleGuideModal(false));
guideModal.addEventListener("click", (e) => {
if (e.target === guideModal) {
toggleGuideModal(false);
}
});
// --- Codebase Modal ---
codebaseBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleCodebaseModal(true);
});
startCodebaseBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleCodebaseModal(true);
});
codebaseModalCloseBtn.addEventListener("click", () => toggleCodebaseModal(false));
codebaseModal.addEventListener("click", (e) => {
if (e.target === codebaseModal) {
toggleCodebaseModal(false);
}
});
// --- Changelog Modal ---
changelogBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleChangelogModal(true);
});
startChangelogBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleChangelogModal(true);
});
changelogModalCloseBtn.addEventListener("click", () => toggleChangelogModal(false));
changelogModal.addEventListener("click", (e) => {
if (e.target === changelogModal) {
toggleChangelogModal(false);
}
});
// Global Key Listener for 'k' and 'ESC'
document.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() === "k") {
// Toggle visibility
const isHidden = shortcutsModal.classList.contains("hidden");
toggleShortcutsModal(isHidden);
}
// Prioritize closing open modals
if (e.key === "Escape") {
if (!guideModal.classList.contains("hidden")) {
toggleGuideModal(false);
} else if (!codebaseModal.classList.contains("hidden")) {
toggleCodebaseModal(false);
} else if (!changelogModal.classList.contains("hidden")) {
toggleChangelogModal(false);
} else if (!shortcutsModal.classList.contains("hidden")) {
toggleShortcutsModal(false);
}
}
});
// --- Menu and Fullscreen ---
toggleMenuBtn.addEventListener("click", () => toggleMenu(true));
closeMenuBtn.addEventListener("click", () => toggleMenu(false));
menuScrim.addEventListener("click", () => toggleMenu(false));
fullscreenBtn.addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else if (document.exitFullscreen) {
document.exitFullscreen();
}
});
// --- Timeline Tooltip ---
timelineSlider.addEventListener("mouseover", () => {
if (appState.vizData) timelineTooltip.classList.remove("hidden");
});
timelineSlider.addEventListener("mouseout", () => {
timelineTooltip.classList.add("hidden");
});
timelineSlider.addEventListener("mousemove", (event) => {
if (!appState.vizData) return;
const rect = timelineSlider.getBoundingClientRect();
const hoverFraction = (event.clientX - rect.left) / rect.width;
const sliderMax = parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1;
let frameIndex = Math.max(0, Math.min(Math.round(hoverFraction * sliderMax), sliderMax));
const frameData = appState.vizData.radarFrames[frameIndex];
if (!frameData) return;
const formattedTime = formatTime(frameData.relativeTimeSec * 1000);
timelineTooltip.innerHTML = `Frame: ${frameIndex + 1}<br>Time: ${formattedTime}`;
const tooltipX = event.clientX - rect.left;
timelineTooltip.style.left = `${tooltipX}px`;
});
// --- Speed Slider ---
speedSlider.addEventListener("input", (event) => {
const speed = parseFloat(event.target.value);
videoPlayer.playbackRate = speed;
speedDisplay.textContent = `${speed.toFixed(1)}x`;
});
// --- SNR Controls ---
applySnrBtn.addEventListener("click", () => {
const newMin = parseFloat(snrMinInput.value), newMax = parseFloat(snrMaxInput.value);
if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) {
showModal("Invalid SNR range.");
return;
}
appState.globalMinSnr = newMin;
appState.globalMaxSnr = newMax;
toggleFrameNorm.checked = false;
if (appState.p5_instance) {
appState.p5_instance.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
appState.p5_instance.redraw();
}
});
// --- Feature Toggles ---
[toggleSnrColor, toggleClusterColor, toggleInlierColor, toggleStationaryColor].forEach((t) => {
t.addEventListener("change", handleColorToggles);
});
[toggleVelocity, toggleEgoSpeed, toggleFrameNorm, toggleTracks, toggleDebugOverlay, toggleDebug2Overlay, toggleCovariance, toggleVehicleDimensions].forEach((t) => {
t.addEventListener("change", () => {
if (appState.p5_instance) appState.p5_instance.redraw();
if (t === toggleDebugOverlay || t === toggleDebug2Overlay) {
updateDebugOverlay(videoPlayer.currentTime);
updatePersistentOverlays(videoPlayer.currentTime);
}
});
});
toggleCloseUp.addEventListener("change", () => {
appState.isCloseUpMode = toggleCloseUp.checked;
appState.zoomPanelExplicitlyClosed = false; // Reset the close flag so it can reappear
// Auto-hide the panel when the user disables Close-Up mode (e.g. by pressing 'g')
if (!appState.isCloseUpMode) {
const zoomPanel = document.getElementById("zoom-panel");
if (zoomPanel && !zoomPanel.classList.contains("hidden")) {
zoomPanel.classList.add("hidden");
}
}
if (appState.isCloseUpMode && appState.isPlaying) {
// If entering close-up mode while playing, automatically pause.
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
if (appState.p5_instance) { // Handle p5 loop state
if (appState.isCloseUpMode) {
appState.p5_instance.loop(); // Start looping for mouse interaction.
} else {
appState.p5_instance.noLoop(); // Stop looping when exiting.
appState.p5_instance.redraw(); // Redraw one last time.
}
}
});
toggleConfirmedOnly.addEventListener("change", () => {
if (appState.p5_instance) appState.p5_instance.redraw();
});
}

65
steps/src/utils.js

@ -1,27 +1,35 @@
export function findRadarFrameIndexForTime(targetTimeMs, vizData) {
/**
* Performs a binary search on the radar frames to find the frame index
* closest to the target video time.
*
* @param {number} targetTimeSec - The target time in seconds (from video.currentTime).
* @param {object} vizData - The visualization data containing radarFrames.
* @returns {number} The index of the closest radar frame.
*/
export function findRadarFrameIndexForTime(targetTimeSec, vizData) {
if (!vizData || vizData.radarFrames.length === 0) return -1;
// Initialize low, high, and answer variables for binary search
// 'ans' will store the index of the closest frame found so far
// 'low' and 'high' define the search range
let low = 0,
high = vizData.radarFrames.length - 1,
ans = 0;
high = vizData.radarFrames.length - 1;
// Perform binary search to find the radar frame whose timestamp is closest to, but not exceeding, the target time
while (low <= high) {
let mid = Math.floor((low + high) / 2);
// If the current frame's timestamp is less than or equal to the target time,
// it's a potential answer, and we try to find a more recent one in the right half.
if (vizData.radarFrames[mid].timestampMs <= targetTimeMs) {
ans = mid;
const frameTime = vizData.radarFrames[mid].videoSyncedTime;
if (frameTime < targetTimeSec) {
low = mid + 1;
} else {
// If the current frame's timestamp is greater than the target time,
// we need to look in the left half.
} else if (frameTime > targetTimeSec) {
high = mid - 1;
} else {
// Exact match found
return mid;
}
}
// Return the index of the found radar frame.
return ans;
// No exact match, return the closest index (clamped to bounds)
return Math.max(0, Math.min(high, vizData.radarFrames.length - 1));
}
@ -44,13 +52,20 @@ export function extractTimestampInfo(filename) {
const timestamp = `${match[1]}_${match[2]}${match[3]}${match[4]}`;
return { timestampStr: timestamp, format: "video" };
}
// Try to match another common video filename pattern: "video_YYYYMMDD_HHMMSS"
match = filename.match(/video_(\d{8}_\d{6})/);
if (match)
return {
timestampStr: match[1],
format: "video",
};
// Try to match generic YYYYMMDD_HHMMSS or similar patterns anywhere in the name
// Examples: video_20231027_103000, cam_20260312_163310, 20260312163310
match = filename.match(/((?:19|20)\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[-_]?([01]\d|2[0-3])([0-5]\d)([0-5]\d)/);
if (match) {
const timestamp = `${match[1]}${match[2]}${match[3]}_${match[4]}${match[5]}${match[6]}`;
return { timestampStr: timestamp, format: "video" };
}
// Try generic DDMMYYYY_HHMMSS pattern just in case
match = filename.match(/(0[1-9]|[12]\d|3[01])(0[1-9]|1[0-2])((?:19|20)\d{2})[-_]?([01]\d|2[0-3])([0-5]\d)([0-5]\d)/);
if (match) {
const timestamp = `${match[3]}${match[2]}${match[1]}_${match[4]}${match[5]}${match[6]}`;
return { timestampStr: timestamp, format: "video" };
}
// If no pattern matches, return null
return null;
}
@ -145,3 +160,15 @@ export function formatUTCTime(date) {
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
/**
* Pre-calculates the video-synchronized timestamp for each radar frame.
* This "bakes" the offset into the data, simplifying future sync calculations.
*
* @param {object} vizData - The visualization data containing radarFrames.
* @param {number} offsetMs - The time offset between radar and video in milliseconds.
*/
export function precomputeRadarVideoSync(vizData, offsetMs) {
vizData.radarFrames.forEach((frame) => {
frame.videoSyncedTime = (frame.timestamp + offsetMs) / 1000;
});
}

172
steps/tests/fileLoader.test.js

@ -0,0 +1,172 @@
import { handleFiles } from "../src/fileLoader.js";
import { appState } from "../src/state.js";
import { initDB } from "../src/db.js";
const resultsEl = document.getElementById('results');
function test(description, testFunction) {
// Simple async test runner wrapper
(async () => {
try {
await testFunction();
console.log(`✅ PASS: ${description}`);
resultsEl.innerHTML += `<p class="pass"><b>PASS:</b> ${description}</p>`;
} catch (error) {
console.error(`❌ FAIL: ${description}`, error);
resultsEl.innerHTML += `<p class="fail"><b>FAIL:</b> ${description}<br><pre>${error.stack || error}</pre></p>`;
}
})();
}
// --- Setup & Mocks ---
// Initialize DB for tests
async function setupTestEnvironment() {
return new Promise((resolve) => {
initDB(() => {
console.log("Test DB initialized");
resolve();
});
});
}
// Mock URL.createObjectURL
URL.createObjectURL = (blob) => {
return "blob:mock-url-" + Math.random();
};
URL.revokeObjectURL = () => {};
// Mock Worker
class MockWorker {
constructor(scriptUrl) {
console.log("MockWorker created for:", scriptUrl);
this.onmessage = null;
}
postMessage(msg) {
console.log("MockWorker received message:", msg);
// Simulate success response
if (this.onmessage) {
// Simulate parsing delay
setTimeout(() => {
this.onmessage({
data: {
type: 'complete',
data: {
// Correct mock parsed data structure
radarFrames: [
{
timestamp: 1000,
pointCloud: [],
tracks: []
}
],
tracks: []
}
}
});
}, 50);
}
}
terminate() {}
}
window.Worker = MockWorker;
// Mock p5
window.p5 = class MockP5 {
constructor(sketch, node) {
console.log("MockP5 created");
sketch(this);
}
createCanvas() { return { parent: () => {} }; }
background() {}
fill() {}
stroke() {}
rect() {}
ellipse() {}
push() {}
pop() {}
translate() {}
scale() {}
frameRate() {}
noLoop() {}
loop() {}
redraw() {}
resizeCanvas() {}
select() { return { html: () => {}, position: () => {}, style: () => {} }; }
createGraphics() { return { background: () => {}, clear: () => {}, image: () => {} }; }
image() {}
text() {}
textSize() {}
textAlign() {}
noStroke() {}
color() { return {}; }
textFont() {}
drawSnrLegendToBuffer() {}
};
// --- Tests ---
(async function runTests() {
await setupTestEnvironment();
test("fileLoader.js: handleFiles should parse JSON and update appState", async () => {
// 1. Setup
appState.vizData = null;
const mockJsonFile = new File(['{"some": "json"}'], "test_data.json", { type: "application/json" });
// 2. Execution
handleFiles([mockJsonFile]);
// 3. Verification (Wait for async operations)
// We need to wait long enough for DB save + Worker + Processing
await new Promise(resolve => setTimeout(resolve, 500));
if (!appState.vizData) {
throw new Error("appState.vizData was not populated after loading JSON.");
}
if (appState.jsonFilename !== "test_data.json") {
throw new Error(`Expected jsonFilename to be 'test_data.json', got '${appState.jsonFilename}'`);
}
});
test("fileLoader.js: handleFiles should handle video loading (simulated)", async () => {
// 1. Setup
appState.vizData = null;
appState.videoFilename = "";
const videoPlayer = document.getElementById('video-player');
// Use MutationObserver to watch for src changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "attributes" && mutation.attributeName === "src") {
console.log("Video src changed, triggering events...");
// Trigger events asynchronously to simulate browser behavior
setTimeout(() => {
videoPlayer.dispatchEvent(new Event('loadedmetadata'));
videoPlayer.dispatchEvent(new Event('canplaythrough'));
}, 50);
}
});
});
observer.observe(videoPlayer, { attributes: true });
const mockVideoFile = new File(['fake video content'], "test_video.mp4", { type: "video/mp4" });
// 2. Execute
handleFiles([mockVideoFile]);
// 3. Verify
await new Promise(resolve => setTimeout(resolve, 500)); // Wait for events
observer.disconnect(); // Cleanup
if (appState.videoFilename !== "test_video.mp4") {
throw new Error(`Expected videoFilename to be 'test_video.mp4', got '${appState.videoFilename}'`);
}
});
})();

73
steps/tests/regression_video_only.test.js

@ -0,0 +1,73 @@
import { handleFiles } from "../src/fileLoader.js";
import { appState } from "../src/state.js";
import { initDB } from "../src/db.js";
const resultsEl = document.getElementById('results');
function test(description, testFunction) {
(async () => {
try {
await testFunction();
console.log(`✅ PASS: ${description}`);
resultsEl.innerHTML += `<p class="pass"><b>PASS:</b> ${description}</p>`;
} catch (error) {
console.error(`❌ FAIL: ${description}`, error);
resultsEl.innerHTML += `<p class="fail"><b>FAIL:</b> ${description}<br><pre>${error.stack || error}</pre></p>`;
}
})();
}
// Mocking dependencies for regression test
async function setupMocks() {
return new Promise((resolve) => {
initDB(() => {
console.log("Test DB initialized");
resolve();
});
});
}
URL.createObjectURL = () => "blob:mock-video-url";
URL.revokeObjectURL = () => {};
window.p5 = class MockP5 {
constructor(sketch) { sketch(this); }
createCanvas() { return { parent: () => {} }; }
noLoop() {}
redraw() {}
};
test("Regression: handleFiles should not crash when loading only a video", async () => {
// 1. Setup - clear vizData
appState.vizData = null;
appState.videoFilename = "";
const videoPlayer = document.getElementById('video-player');
const mockVideoFile = new File(['fake video'], "test.mp4", { type: "video/mp4" });
// Mock video events
const triggerEvents = () => {
setTimeout(() => {
videoPlayer.dispatchEvent(new Event('loadedmetadata'));
videoPlayer.dispatchEvent(new Event('canplaythrough'));
}, 10);
};
const observer = new MutationObserver(triggerEvents);
observer.observe(videoPlayer, { attributes: true, attributeFilter: ['src'] });
// 2. Execution
try {
await handleFiles([mockVideoFile]);
} catch (e) {
throw new Error(`Crash detected during video-only load: ${e.message}`);
}
// 3. Verification
await new Promise(resolve => setTimeout(resolve, 100));
observer.disconnect();
if (appState.videoFilename !== "test.mp4") {
throw new Error("Video filename not set correctly in appState");
}
});

62
steps/tests/simple_log_cfg.py

@ -0,0 +1,62 @@
import re
import tkinter as tk
from tkinter import filedialog
import os
def extract_radar_config(log_file_path, output_file_path=None):
"""
Extract radar configuration commands from log file.
Args:
log_file_path (str): Path to input log file
output_file_path (str, optional): Path to save extracted config
"""
# Pattern to match required lines
pattern = re.compile(r'INFO\s+-\s+radar_tracker\.console_logger\s+-\s+>\s+(.*)')
extracted_commands = []
with open(log_file_path, 'r') as file:
for line in file:
match = pattern.search(line)
if match:
command = match.group(1).strip()
extracted_commands.append(command)
# Output handling
if output_file_path:
with open(output_file_path, 'w') as out_file:
for cmd in extracted_commands:
out_file.write(cmd + '\n')
print(f"[INFO] Extracted config saved to: {output_file_path}")
else:
print("\n--- Extracted Radar Config ---\n")
for cmd in extracted_commands:
print(cmd)
return extracted_commands
# Example usage
if __name__ == "__main__":
# Create and hide root tkinter window
root = tk.Tk()
root.withdraw()
# Allow user to select a .log file
log_file = filedialog.askopenfilename(
title="Select Log File",
filetypes=[("Log Files", "*.log"), ("Text Files", "*.txt"), ("All Files", "*.*")]
)
if log_file:
# Generate output file name (e.g., myscript.log -> myscript.cfg)
base_name = os.path.splitext(log_file)[0]
output_file = f"{base_name}.cfg"
extract_radar_config(log_file, output_file)
print(f"[INFO] Processing complete for {log_file}")
else:
print("[INFO] No file selected.")

87
steps/tests/test-runner.html

@ -15,8 +15,93 @@
<p>Check the browser's console for detailed results.</p>
<div id="results"></div>
<script src="../vendor/p5.js"></script>
<script type="module" src="utils.test.js"></script>
<script type="module" src="fileParsers.test.js"></script> </body>
<script type="module" src="fileParsers.test.js"></script>
<script type="module" src="fileLoader.test.js"></script>
<!-- Mock DOM for dom.js -->
<div id="mock-dom" style="display: none;">
<button id="theme-toggle"></button>
<div id="canvas-container"></div>
<div id="canvas-placeholder"></div>
<video id="video-player"></video>
<div id="video-placeholder"></div>
<button id="load-json-btn"></button>
<button id="load-video-btn"></button>
<button id="load-can-btn"></button>
<input id="json-file-input" type="file">
<input id="video-file-input" type="file">
<input id="can-file-input" type="file">
<button id="play-pause-btn"></button>
<button id="stop-btn"></button>
<input id="timeline-slider" type="range">
<div id="frame-counter"></div>
<input id="offset-input">
<input id="speed-slider" type="range">
<div id="speed-display"></div>
<div id="feature-toggles">
<input type="checkbox" id="toggle-snr-color">
<input type="checkbox" id="toggle-cluster-color">
<input type="checkbox" id="toggle-inlier-color">
<input type="checkbox" id="toggle-stationary-color">
<input type="checkbox" id="toggle-velocity">
<input type="checkbox" id="toggle-tracks">
<input type="checkbox" id="toggle-ego-speed">
<input type="checkbox" id="toggle-frame-norm">
<input type="checkbox" id="toggle-debug-overlay">
<input type="checkbox" id="toggle-debug2-overlay">
<input type="checkbox" id="toggle-close-up">
<input type="checkbox" id="toggle-predicted-pos">
<input type="checkbox" id="toggle-covariance">
<input type="checkbox" id="toggle-confirmed-only">
<input type="checkbox" id="ttc-mode-default">
<input type="checkbox" id="ttc-mode-custom">
</div>
<div id="ego-speed-display"></div>
<div id="can-speed-display"></div>
<div id="debug-overlay"></div>
<input id="snr-min-input">
<input id="snr-max-input">
<button id="apply-snr-btn"></button>
<div id="auto-offset-indicator"></div>
<button id="clear-cache-btn"></button>
<div id="speed-graph-container"></div>
<div id="speed-graph-placeholder"></div>
<div id="modal-container"></div>
<div id="modal-overlay"></div>
<div id="modal-content"></div>
<div id="modal-text"></div>
<button id="modal-ok-btn"></button>
<button id="modal-cancel-btn"></button>
<div id="modal-progress-container"></div>
<div id="modal-progress-bar"></div>
<div id="modal-progress-text"></div>
<div id="timeline-tooltip"></div>
<div id="radar-info-overlay"></div>
<div id="video-info-overlay"></div>
<button id="save-session-btn"></button>
<button id="load-session-btn"></button>
<input id="session-file-input" type="file">
<div id="custom-ttc-panel"></div>
<input id="ttc-color-critical">
<input id="ttc-time-critical">
<input id="ttc-color-high">
<input id="ttc-time-high">
<input id="ttc-color-medium">
<input id="ttc-time-medium">
<input id="ttc-color-low">
<input id="ttc-time-low">
<div id="collapsible-menu"></div>
<button id="toggle-menu-btn"></button>
<button id="fullscreen-btn"></button>
<main></main>
<button id="close-menu-btn"></button>
<div id="fullscreen-enter-icon"></div>
<div id="fullscreen-exit-icon"></div>
<div id="menu-scrim"></div>
<button id="explorer-btn"></button>
<div id="zoom-canvas-container"></div>
</div>
</body>
</html>

3
steps/vendor/gridstack-all.js
File diff suppressed because it is too large
View File

1
steps/vendor/gridstack.min.css

@ -0,0 +1 @@
.grid-stack{position:relative}.grid-stack-rtl{direction:ltr}.grid-stack-rtl>.grid-stack-item{direction:rtl}.grid-stack-placeholder>.placeholder-content{background-color:rgba(0,0,0,.1);margin:0;position:absolute;width:auto;z-index:0!important}.grid-stack>.grid-stack-item{position:absolute;padding:0}.grid-stack>.grid-stack-item>.grid-stack-item-content{margin:0;position:absolute;width:auto;overflow-x:hidden;overflow-y:auto}.grid-stack>.grid-stack-item.size-to-content:not(.size-to-content-max)>.grid-stack-item-content{overflow-y:hidden}.grid-stack-item>.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.grid-stack-item.ui-resizable-autohide>.ui-resizable-handle,.grid-stack-item.ui-resizable-disabled>.ui-resizable-handle{display:none}.grid-stack-item>.ui-resizable-ne,.grid-stack-item>.ui-resizable-nw,.grid-stack-item>.ui-resizable-se,.grid-stack-item>.ui-resizable-sw{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%23666" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 20 20"><path d="m10 3 2 2H8l2-2v14l-2-2h4l-2 2"/></svg>');background-repeat:no-repeat;background-position:center}.grid-stack-item>.ui-resizable-ne{transform:translate(0,10px) rotate(45deg)}.grid-stack-item>.ui-resizable-sw{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-nw{transform:translate(0,10px) rotate(-45deg)}.grid-stack-item>.ui-resizable-se{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-nw{cursor:nw-resize;width:20px;height:20px;top:0}.grid-stack-item>.ui-resizable-n{cursor:n-resize;height:10px;top:0;left:25px;right:25px}.grid-stack-item>.ui-resizable-ne{cursor:ne-resize;width:20px;height:20px;top:0}.grid-stack-item>.ui-resizable-e{cursor:e-resize;width:10px;top:15px;bottom:15px}.grid-stack-item>.ui-resizable-se{cursor:se-resize;width:20px;height:20px}.grid-stack-item>.ui-resizable-s{cursor:s-resize;height:10px;left:25px;bottom:0;right:25px}.grid-stack-item>.ui-resizable-sw{cursor:sw-resize;width:20px;height:20px}.grid-stack-item>.ui-resizable-w{cursor:w-resize;width:10px;top:15px;bottom:15px}.grid-stack-item.ui-draggable-dragging>.ui-resizable-handle{display:none!important}.grid-stack-item.ui-draggable-dragging{will-change:left,top;cursor:move}.grid-stack-item.ui-resizable-resizing{will-change:width,height}.ui-draggable-dragging,.ui-resizable-resizing{z-index:10000}.ui-draggable-dragging>.grid-stack-item-content,.ui-resizable-resizing>.grid-stack-item-content{box-shadow:1px 4px 6px rgba(0,0,0,.2);opacity:.8}.grid-stack-animate,.grid-stack-animate .grid-stack-item{transition:left .3s,top .3s,height .3s,width .3s}.grid-stack-animate .grid-stack-item.grid-stack-placeholder,.grid-stack-animate .grid-stack-item.ui-draggable-dragging,.grid-stack-animate .grid-stack-item.ui-resizable-resizing{transition:left 0s,top 0s,height 0s,width 0s}.grid-stack>.grid-stack-item[gs-y="0"]{top:0}.grid-stack>.grid-stack-item[gs-x="0"]{left:0}.gs-12>.grid-stack-item{width:8.333%}.gs-12>.grid-stack-item[gs-x="1"]{left:8.333%}.gs-12>.grid-stack-item[gs-w="2"]{width:16.667%}.gs-12>.grid-stack-item[gs-x="2"]{left:16.667%}.gs-12>.grid-stack-item[gs-w="3"]{width:25%}.gs-12>.grid-stack-item[gs-x="3"]{left:25%}.gs-12>.grid-stack-item[gs-w="4"]{width:33.333%}.gs-12>.grid-stack-item[gs-x="4"]{left:33.333%}.gs-12>.grid-stack-item[gs-w="5"]{width:41.667%}.gs-12>.grid-stack-item[gs-x="5"]{left:41.667%}.gs-12>.grid-stack-item[gs-w="6"]{width:50%}.gs-12>.grid-stack-item[gs-x="6"]{left:50%}.gs-12>.grid-stack-item[gs-w="7"]{width:58.333%}.gs-12>.grid-stack-item[gs-x="7"]{left:58.333%}.gs-12>.grid-stack-item[gs-w="8"]{width:66.667%}.gs-12>.grid-stack-item[gs-x="8"]{left:66.667%}.gs-12>.grid-stack-item[gs-w="9"]{width:75%}.gs-12>.grid-stack-item[gs-x="9"]{left:75%}.gs-12>.grid-stack-item[gs-w="10"]{width:83.333%}.gs-12>.grid-stack-item[gs-x="10"]{left:83.333%}.gs-12>.grid-stack-item[gs-w="11"]{width:91.667%}.gs-12>.grid-stack-item[gs-x="11"]{left:91.667%}.gs-12>.grid-stack-item[gs-w="12"]{width:100%}.gs-1>.grid-stack-item{width:100%}

5
steps/vendor/prism.css

@ -0,0 +1,5 @@
/**
* PrismJS 1.29.0
* https://prismjs.com/download.html#themes=prism-okaidia
*/
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}

1
steps/vendor/prism.js
File diff suppressed because it is too large
View File

574
zoomsketch-issue/ADAS/ARAS Pipeline.html

@ -0,0 +1,574 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ADAS/ARAS Development for Indian Roads</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<!--
Palette Name: Energetic & Playful (Adapted for Professional Data)
Colors:
- Primary Navy: #003f5c
- Deep Purple: #58508d
- Vibrant Pink: #bc5090
- Alert Red: #ff6361
- Warning Yellow: #ffa600
-->
<style>
body {
font-family: 'Roboto', sans-serif;
background-color: #f3f4f6;
color: #1f2937;
}
/* Chart Container Styling - MANDATORY */
.chart-container {
position: relative;
width: 100%;
margin-left: auto;
margin-right: auto;
background-color: white;
border-radius: 0.5rem;
padding: 1rem;
}
/* Specific constraints for responsiveness */
.chart-box-lg {
height: 400px;
max-height: 500px;
max-width: 800px;
}
.chart-box-md {
height: 300px;
max-height: 400px;
max-width: 600px;
}
/* Scrollable Table Styling */
.scenarios-table-container {
max-height: 600px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #003f5c #e5e7eb;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #58508d;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #003f5c;
}
/* Diagram Utilities (CSS-only Flowchart) */
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 1rem;
background: white;
border-left: 4px solid #003f5c;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border-radius: 0.375rem;
width: 100%;
}
.flow-arrow {
font-size: 2rem;
color: #bc5090;
margin: 0.5rem 0;
line-height: 1;
}
</style>
<!--
CRITICAL:
- Source Material: User provided context on ADAS/ARAS workflow + Request for 50 Scenarios.
- Narrative Plan:
1. Context & Challenge (Indian Roads).
2. The 50 Scenarios (Data Table + Distribution Analysis).
3. Workflow Optimization (Current vs. Future State).
4. Technical Recommendations (Tools).
- Visualizations:
1. Doughnut Chart (Scenario Categories) -> Goal: Inform composition.
2. Bubble Chart (Frequency vs Severity) -> Goal: Relationships/Prioritization.
3. HTML Table -> Goal: Organize the 50 items.
4. Radar Chart -> Goal: Compare Workflow Attributes.
5. CSS Flowchart -> Goal: Process Flow (NO MERMAID/SVG).
- NO SVG Used. NO Mermaid Used.
-->
</head>
<body class="bg-gray-100">
<!-- Header -->
<header class="bg-[#003f5c] text-white py-8 shadow-lg">
<div class="container mx-auto px-4">
<h1 class="text-3xl md:text-4xl font-bold mb-2">ADAS & ARAS Development: Indian Context</h1>
<p class="text-xl text-gray-200">Accelerating FCW & BSD Validation for 2/3-Wheelers</p>
</div>
</header>
<main class="container mx-auto px-4 py-8 space-y-12">
<!-- Section 1: Introduction -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="md:col-span-2 bg-white p-6 rounded-lg shadow-md border-t-4 border-[#bc5090]">
<h2 class="text-2xl font-bold text-[#003f5c] mb-4">The Challenge: Engineering for Chaos</h2>
<p class="text-gray-700 leading-relaxed mb-4">
Developing Advanced Driver Assistance Systems (ADAS) for India requires moving beyond standard Euro NCAP definitions. The environment is characterized by high density, heterogeneous traffic (trucks sharing lanes with bicycles), and unpredictable road geometry.
</p>
<p class="text-gray-700 leading-relaxed">
The current workflow utilizes <strong>AWRL1432/1843 radar sensors</strong> and <strong>MATLAB-based tracking</strong>. To achieve robust Front Collision Warning (FCW) and Blind Spot Detection (BSD), we must validate against specific edge cases found only on Indian roads.
</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md flex flex-col justify-center items-center text-center">
<div class="text-5xl font-bold text-[#ff6361] mb-2">50</div>
<div class="text-lg font-medium text-gray-600">Unique Scenarios Identified</div>
<div class="mt-4 w-full h-1 bg-gray-200 rounded">
<div class="h-1 bg-[#ff6361] rounded" style="width: 100%"></div>
</div>
<p class="text-sm text-gray-500 mt-2">Targeting High-Risk Edge Cases</p>
</div>
</section>
<!-- Section 2: Scenario Analysis -->
<section>
<div class="mb-6">
<h2 class="text-2xl font-bold text-[#003f5c]">Scenario Categorization</h2>
<p class="text-gray-600 mt-2">
Before diving into the list, we analyze the distribution of scenarios. Indian traffic creates a unique cluster of "Lateral" and "Static Obstacle" risks that are less prevalent in western datasets.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<!-- Chart 1: Scenario Distribution -->
<div class="flex flex-col">
<div class="chart-container chart-box-md shadow-md">
<canvas id="scenarioDistChart"></canvas>
</div>
<p class="text-sm text-gray-500 mt-2 italic text-center">Fig 1. Breakdown of the 50 generated scenarios by primary threat type.</p>
</div>
<!-- Chart 2: Priority Matrix (Bubble) -->
<div class="flex flex-col">
<div class="chart-container chart-box-md shadow-md">
<canvas id="riskBubbleChart"></canvas>
</div>
<p class="text-sm text-gray-500 mt-2 italic text-center">Fig 2. Frequency vs. Severity. High severity/frequency items (Top Right) are critical for FCW.</p>
</div>
</div>
<!-- The Big Table -->
<div class="bg-white rounded-lg shadow-md overflow-hidden border-t-4 border-[#ffa600]">
<div class="p-6 bg-gray-50 border-b">
<h3 class="text-xl font-bold text-[#003f5c]">The 50 Critical Scenarios Matrix</h3>
<p class="text-sm text-gray-600">Compiled from Indian accident data reports and ADAS edge-case studies.</p>
</div>
<div class="scenarios-table-container">
<table class="min-w-full text-sm text-left">
<thead class="text-xs text-white uppercase bg-[#003f5c] sticky top-0 z-10">
<tr>
<th class="px-6 py-3">ID</th>
<th class="px-6 py-3">Scenario Name</th>
<th class="px-6 py-3">Type</th>
<th class="px-6 py-3">Indian Context / Description</th>
<th class="px-6 py-3">Priority</th>
</tr>
</thead>
<tbody id="scenarioTableBody" class="divide-y divide-gray-200">
<!-- JS will populate this -->
</tbody>
</table>
</div>
</div>
</section>
<!-- Section 3: Workflow Optimization -->
<section>
<div class="mb-6">
<h2 class="text-2xl font-bold text-[#003f5c]">Workflow Modernization</h2>
<p class="text-gray-600 mt-2">
Moving from a MATLAB-centric, human-in-the-loop workflow to a standardized ROS2 pipeline can reduce iteration time by estimated 40%. The current bottleneck is manual synchronization and the overhead of interpreted code.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Chart 3: Radar Comparison -->
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-lg font-bold text-[#58508d] mb-4">Pipeline Capabilities Comparison</h3>
<div class="chart-container chart-box-md mx-auto">
<canvas id="workflowRadarChart"></canvas>
</div>
<div class="mt-4 text-sm text-gray-600">
<p><span class="inline-block w-3 h-3 bg-[#ff6361] mr-2"></span>Current (MATLAB) is excellent for rapid prototyping but struggles with real-time speed and integration.</p>
<p><span class="inline-block w-3 h-3 bg-[#003f5c] mr-2"></span>Proposed (ROS2/C++) offers superior sensor fusion capabilities and visualization tools.</p>
</div>
</div>
<!-- Process Flow Diagram (CSS only) -->
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-lg font-bold text-[#58508d] mb-4">Proposed Accelerated Workflow</h3>
<div class="flex flex-col items-center space-y-2">
<!-- Step 1 -->
<div class="flow-step border-l-[#ffa600]">
<h4 class="font-bold text-[#003f5c]">1. Data Ingestion (ROS2)</h4>
<p class="text-xs text-gray-500">AWRL1432 Driver Node &rarr; /radar/pointcloud2</p>
</div>
<div class="flow-arrow">&#x2193;</div>
<!-- Step 2 -->
<div class="flow-step border-l-[#ff6361]">
<h4 class="font-bold text-[#003f5c]">2. Auto-Labeling</h4>
<p class="text-xs text-gray-500">Sync Video &rarr; YOLOv8/SAM for Ground Truth Bounding Boxes</p>
</div>
<div class="flow-arrow">&#x2193;</div>
<!-- Step 3 -->
<div class="flow-step border-l-[#bc5090]">
<h4 class="font-bold text-[#003f5c]">3. Algorithm & Fusion</h4>
<p class="text-xs text-gray-500">C++ Tracking Nodes + Camera Fusion (Kalman Filter)</p>
</div>
<div class="flow-arrow">&#x2193;</div>
<!-- Step 4 -->
<div class="flow-step border-l-[#58508d]">
<h4 class="font-bold text-[#003f5c]">4. Visualization (Foxglove)</h4>
<p class="text-xs text-gray-500">Web-based replay of Radar PCL + Video Overlay</p>
</div>
</div>
</div>
</div>
</section>
<!-- Section 4: Recommendations -->
<section class="bg-[#003f5c] rounded-xl p-8 shadow-xl text-white">
<h2 class="text-2xl font-bold mb-6">Actionable Improvements</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Rec 1 -->
<div class="bg-white/10 p-4 rounded border border-white/20 backdrop-blur-sm">
<h3 class="text-[#ffa600] font-bold text-lg mb-2">1. Adopt Foxglove Studio</h3>
<p class="text-sm opacity-90">Replace the custom visualizer. Foxglove allows drag-and-drop visualization of point clouds and camera feeds, perfectly synchronized, via browser.</p>
</div>
<!-- Rec 2 -->
<div class="bg-white/10 p-4 rounded border border-white/20 backdrop-blur-sm">
<h3 class="text-[#ffa600] font-bold text-lg mb-2">2. Automated Ground Truth</h3>
<p class="text-sm opacity-90">Stop manual video comparison. Run the video through a pre-trained model (like YOLOv8) to generate "Ground Truth" boxes, then calculate Intersection over Union (IoU) with radar tracks automatically.</p>
</div>
<!-- Rec 3 -->
<div class="bg-white/10 p-4 rounded border border-white/20 backdrop-blur-sm">
<h3 class="text-[#ffa600] font-bold text-lg mb-2">3. Move to C++/ROS2</h3>
<p class="text-sm opacity-90">MATLAB is great for math, but slow for pipelines. Migrating the tracking logic to C++ nodes in ROS2 ensures the code is ready for embedded deployment.</p>
</div>
</div>
</section>
<footer class="text-center text-gray-500 text-sm mt-8 pb-4">
<p>&copy; 2025 ADAS Research Division. Generated for Internal Review.</p>
</footer>
</main>
<script>
// --- 1. Data Definitions ---
// The 50 Scenarios Data
const scenarios = [
// FCW - Urban/Congestion
{ id: 1, name: "The Auto-Rickshaw Cut-In", type: "FCW", desc: "Auto-rickshaw abruptly changes lane into ego path from left/right at low speed.", priority: "High" },
{ id: 2, name: "Sudden Stop for Pothole", type: "FCW", desc: "Lead vehicle brakes hard unexpectedly to avoid a road defect.", priority: "High" },
{ id: 3, name: "Pedestrian Jaywalking (Group)", type: "FCW", desc: "Group of pedestrians crossing mid-block in dense traffic.", priority: "High" },
{ id: 4, name: "Stray Dog Chasing", type: "FCW", desc: "Animal runs perpendicular to ego vehicle path.", priority: "Med" },
{ id: 5, name: "Cattle on Highway", type: "FCW", desc: "Stationary or slow-moving cow sitting on the median/lane.", priority: "High" },
{ id: 6, name: "Wrong-Way Motorcyclist", type: "FCW", desc: "Bike approaching head-on in the ego lane (shoulder riding).", priority: "Critical" },
{ id: 7, name: "Bus Stop Pull-Out", type: "FCW", desc: "Bus merges from stop without indicator.", priority: "Med" },
{ id: 8, name: "The 'Squeeze' Gap", type: "FCW", desc: "Two heavy vehicles creating a narrowing tunnel for the ego 2-wheeler.", priority: "High" },
{ id: 9, name: "Door Opening", type: "FCW", desc: "Parked car door opens into ego path.", priority: "Med" },
{ id: 10, name: "Intersection Creeper", type: "FCW", desc: "Vehicle inching into intersection during red light.", priority: "Med" },
// BSD - Overtaking & Filtering
{ id: 11, name: "Left-Side Overtake", type: "BSD", desc: "Faster bike overtaking ego vehicle from the left (blind spot).", priority: "Critical" },
{ id: 12, name: "Zig-Zagging Scooter", type: "BSD", desc: "Scooter weaving continuously through traffic behind ego.", priority: "High" },
{ id: 13, name: "Heavy Truck Blind Zone", type: "BSD", desc: "Truck alongside ego vehicle drifting into ego lane.", priority: "Critical" },
{ id: 14, name: "E-Rickshaw Silent Approach", type: "BSD", desc: "Electric rickshaw approaching silently in blind spot.", priority: "High" },
{ id: 15, name: "Cyclist Filtering", type: "BSD", desc: "Cyclist moving between ego vehicle and curb.", priority: "Med" },
// FCW - Highway
{ id: 16, name: "Unmarked Speed Breaker", type: "FCW", desc: "Lead vehicle becomes airborne/brakes hard for invisible hump.", priority: "High" },
{ id: 17, name: "Tractor Trolley (No Lights)", type: "FCW", desc: "Slow moving agricultural vehicle at night with no reflectors.", priority: "Critical" },
{ id: 18, name: "Construction Debris", type: "FCW", desc: "Pile of sand/bricks left on the carriageway.", priority: "Med" },
{ id: 19, name: "Broken Down Truck", type: "FCW", desc: "Stationary truck in fast lane without hazard lights.", priority: "Critical" },
{ id: 20, name: "Toll Plaza Queue", type: "FCW", desc: "Approaching stationary queue at high speed.", priority: "Low" },
// BSD - Turning
{ id: 21, name: "The 'U-Turn' Conflict", type: "BSD", desc: "Vehicle performing U-turn hits ego vehicle's rear flank.", priority: "High" },
{ id: 22, name: "Free Left Turn Merge", type: "BSD", desc: "Vehicle merging from free left turn into ego's blind spot.", priority: "Med" },
{ id: 23, name: "Roundabout Exit Cut", type: "BSD", desc: "Vehicle exiting roundabout cuts across ego's front/side.", priority: "Med" },
// Environmental / Sensor Noise
{ id: 24, name: "Heavy Rain Clutter", type: "Noise", desc: "Monsoon rain creating false positives in radar.", priority: "Med" },
{ id: 25, name: "Metal Fence Reflection", type: "Noise", desc: "Guard rails on curves interpreted as dynamic objects.", priority: "Low" },
{ id: 26, name: "Tunnel Entry/Exit", type: "Light", desc: "Sudden lighting change affecting camera verification.", priority: "Low" },
// More FCW/BSD Mix
{ id: 27, name: "Vegetable Cart", type: "FCW", desc: "Hand-pushed cart moving at walking pace in lane.", priority: "Med" },
{ id: 28, name: "Tailgating SUV", type: "BSD", desc: "Large vehicle following too closely, disappearing from mirrors.", priority: "High" },
{ id: 29, name: "Water Tanker Spillage", type: "FCW", desc: "Wet road surface causing lead vehicle to slip.", priority: "Low" },
{ id: 30, name: "Emergency Vehicle", type: "BSD", desc: "Ambulance approaching fast from rear quarter.", priority: "High" },
// Filling to 50 with variations
{ id: 31, name: "Median Jumper (Pedestrian)", type: "FCW", desc: "Person jumping over median divider.", priority: "High" },
{ id: 32, name: "Merging Traffic (High Speed)", type: "BSD", desc: "Highway on-ramp merge at unsafe speed.", priority: "Med" },
{ id: 33, name: "Sudden Lane Expansion", type: "FCW", desc: "Traffic fanning out causing erratic lateral movements.", priority: "Low" },
{ id: 34, name: "Auto-Rickshaw U-Turn", type: "FCW", desc: "3-wheeler making tight U-turn on narrow road.", priority: "High" },
{ id: 35, name: "Loose Gravel Skid", type: "FCW", desc: "Lead bike skids on gravel.", priority: "Med" },
{ id: 36, name: "Police Barricade", type: "FCW", desc: "Unmarked zigzag barricades on highway.", priority: "Critical" },
{ id: 37, name: "Overloaded Truck Sway", type: "BSD", desc: "Cargo extending beyond truck width into ego lane.", priority: "High" },
{ id: 38, name: "Child on Road", type: "FCW", desc: "Small radar cross-section target (child).", priority: "Critical" },
{ id: 39, name: "Fog/Smog Visibility", type: "Noise", desc: "Reduced visibility scenarios for camera validation.", priority: "Med" },
{ id: 40, name: "Garbage Dump", type: "FCW", desc: "Pile of garbage extending onto road.", priority: "Med" },
{ id: 41, name: "Hawker on Roadside", type: "BSD", desc: "Stationary seller forcing traffic to swerve into ego.", priority: "Med" },
{ id: 42, name: "Shadow Contrast", type: "Light", desc: "Deep shadows under flyovers confusing tracking.", priority: "Low" },
{ id: 43, name: "Motorcycle Convoy", type: "BSD", desc: "Group of bikers surrounding ego vehicle.", priority: "High" },
{ id: 44, name: "Bus Stopping Mid-Road", type: "FCW", desc: "Bus stops to let passengers off not at a stop.", priority: "High" },
{ id: 45, name: "Reversing Vehicle", type: "FCW", desc: "Car reversing on main road due to missed turn.", priority: "Critical" },
{ id: 46, name: "Hanging Cables", type: "FCW", desc: "Low hanging wires (radar ghost target).", priority: "Low" },
{ id: 47, name: "Manhole Open", type: "FCW", desc: "Open manhole requiring sharp evasion.", priority: "Critical" },
{ id: 48, name: "Speeding Delivery Bike", type: "BSD", desc: "Swiggy/Zomato rider cutting aggressively.", priority: "High" },
{ id: 49, name: "Follow-Me Car", type: "FCW", desc: "Vehicle with erratic speed profile.", priority: "Med" },
{ id: 50, name: "Traffic Police Signal", type: "FCW", desc: "Hand gesture stop (not detected by radar).", priority: "Med" }
];
// --- 2. Utility Functions ---
// Label Wrapping Helper
function wrapLabel(str, maxLen) {
if (str.length <= maxLen) return str;
const words = str.split(' ');
const lines = [];
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
if ((currentLine + " " + words[i]).length < maxLen) {
currentLine += " " + words[i];
} else {
lines.push(currentLine);
currentLine = words[i];
}
}
lines.push(currentLine);
return lines;
}
// Common Chart Options
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { font: { family: 'Roboto' } }
},
tooltip: {
backgroundColor: 'rgba(0, 63, 92, 0.9)',
titleFont: { size: 14, family: 'Roboto' },
bodyFont: { size: 13, family: 'Roboto' },
padding: 12,
callbacks: {
title: function(tooltipItems) {
const item = tooltipItems[0];
let label = item.chart.data.labels[item.dataIndex];
if (Array.isArray(label)) {
return label.join(' ');
} else {
return label;
}
}
}
}
}
};
// --- 3. Chart Generation ---
// Chart 1: Scenario Distribution (Doughnut)
function renderDistChart() {
const counts = { FCW: 0, BSD: 0, Noise: 0, Light: 0 };
scenarios.forEach(s => counts[s.type] = (counts[s.type] || 0) + 1);
const ctx = document.getElementById('scenarioDistChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['FCW Scenarios', 'BSD Scenarios', 'Sensor Noise', 'Lighting/Env'],
datasets: [{
data: [counts.FCW, counts.BSD, counts.Noise, counts.Light],
backgroundColor: ['#003f5c', '#bc5090', '#ffa600', '#ff6361'],
borderWidth: 0
}]
},
options: {
...commonOptions,
cutout: '60%',
plugins: {
...commonOptions.plugins,
title: {
display: true,
text: 'Scenario Composition',
font: { size: 16 }
}
}
}
});
}
// Chart 2: Risk Bubble Chart (Frequency vs Severity)
function renderBubbleChart() {
const ctx = document.getElementById('riskBubbleChart').getContext('2d');
// Mapping categories to approximate Frequency (X) and Severity (Y) for visualization
// High Priority = High Severity (Y > 8)
// Common Indian Scenario = High Frequency (X > 7)
const dataPoints = [
{ x: 9, y: 9, r: 15, label: 'Wrong-Way Driver' },
{ x: 9, y: 7, r: 12, label: 'Auto-Rickshaw Cut-In' },
{ x: 8, y: 8, r: 10, label: 'Cattle on Road' },
{ x: 9, y: 6, r: 10, label: 'Left Overtake' },
{ x: 6, y: 9, r: 8, label: 'Unmarked Breaker' },
{ x: 5, y: 9, r: 8, label: 'Broken Truck' },
{ x: 8, y: 4, r: 6, label: 'Jaywalkers' },
{ x: 4, y: 3, r: 5, label: 'Rain Clutter' },
{ x: 3, y: 8, r: 6, label: 'Child' }
];
new Chart(ctx, {
type: 'bubble',
data: {
datasets: [{
label: 'Scenario Risk Analysis',
data: dataPoints,
backgroundColor: dataPoints.map(d => d.y > 8 ? '#ff6361' : '#58508d'),
}]
},
options: {
...commonOptions,
scales: {
x: {
title: { display: true, text: 'Frequency of Occurrence (India)' },
min: 0, max: 10
},
y: {
title: { display: true, text: 'Severity / Safety Risk' },
min: 0, max: 10
}
},
plugins: {
...commonOptions.plugins,
tooltip: {
callbacks: {
label: function(context) {
return context.raw.label + ` (Freq: ${context.raw.x}, Sev: ${context.raw.y})`;
}
}
}
}
}
});
}
// Chart 3: Radar Chart (Workflow Comparison)
function renderRadarChart() {
const ctx = document.getElementById('workflowRadarChart').getContext('2d');
const labels = ['Real-time Performance', 'Sensor Fusion Ease', 'Visualization Tools', 'Debugging Speed', 'Deployment Readiness'];
// Wrap labels
const wrappedLabels = labels.map(l => wrapLabel(l, 16));
new Chart(ctx, {
type: 'radar',
data: {
labels: wrappedLabels,
datasets: [{
label: 'Current (MATLAB)',
data: [4, 5, 5, 8, 3], // MATLAB good at debug, poor at real-time/deploy
fill: true,
backgroundColor: 'rgba(255, 99, 97, 0.2)',
borderColor: '#ff6361',
pointBackgroundColor: '#ff6361',
}, {
label: 'Proposed (ROS2/C++)',
data: [9, 9, 8, 6, 9], // ROS2 great at real-time, fusion, deploy
fill: true,
backgroundColor: 'rgba(0, 63, 92, 0.2)',
borderColor: '#003f5c',
pointBackgroundColor: '#003f5c',
}]
},
options: {
...commonOptions,
scales: {
r: {
angleLines: { color: '#e5e7eb' },
grid: { color: '#e5e7eb' },
pointLabels: {
font: { size: 12, family: 'Roboto' }
},
suggestedMin: 0,
suggestedMax: 10
}
}
}
});
}
// --- 4. Table Population ---
function populateTable() {
const tbody = document.getElementById('scenarioTableBody');
scenarios.forEach(s => {
const tr = document.createElement('tr');
tr.className = "bg-white border-b hover:bg-gray-50";
// Color code priority
let priorityClass = "text-gray-600";
if (s.priority === 'Critical') priorityClass = "text-[#ff6361] font-bold";
if (s.priority === 'High') priorityClass = "text-[#ffa600] font-bold";
tr.innerHTML = `
<td class="px-6 py-4 font-medium text-gray-900">${s.id}</td>
<td class="px-6 py-4 font-semibold text-[#003f5c]">${s.name}</td>
<td class="px-6 py-4"><span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded border border-gray-500">${s.type}</span></td>
<td class="px-6 py-4 text-gray-600">${s.desc}</td>
<td class="px-6 py-4 ${priorityClass}">${s.priority}</td>
`;
tbody.appendChild(tr);
});
}
// --- 5. Initialization ---
window.onload = function() {
renderDistChart();
renderBubbleChart();
renderRadarChart();
populateTable();
};
</script>
</body>
</html>
Loading…
Cancel
Save