Browse Source

Prettier Code and Comments all the way.

refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
eb58f9bf7b
  1. 492
      steps/index.html
  2. 43
      steps/readme.md
  3. 9
      steps/src/constants.js
  4. 108
      steps/src/db.js
  5. 68
      steps/src/dom.js
  6. 721
      steps/src/drawUtils.js
  7. 197
      steps/src/fileParsers.js
  8. 83
      steps/src/main.js
  9. 73
      steps/src/modal.js
  10. 264
      steps/src/p5/radarSketch.js
  11. 407
      steps/src/p5/speedGraphSketch.js
  12. 53
      steps/src/state.js
  13. 96
      steps/src/sync.js
  14. 90
      steps/src/theme.js
  15. 181
      steps/src/utils.js

492
steps/index.html

@ -3,280 +3,268 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Radar and Video Visualizer - Timestamp Synchronized</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- DARK MODE: Step 1 - Configure Tailwind to use the 'class' strategy for dark mode -->
<script>
tailwind.config = {
darkMode: "class",
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></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&family=Roboto+Mono:wght@400;500&display=swap"
rel="stylesheet" />
<style>
body {
font-family: "Inter", sans-serif;
}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Radar and Video Visualizer - Timestamp Synchronized</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- DARK MODE: Step 1 - Configure Tailwind to use the 'class' strategy for dark mode -->
<script>
tailwind.config = {
darkMode: "class",
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></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&family=Roboto+Mono:wght@400;500&display=swap"
rel="stylesheet" />
<style>
body {
font-family: "Inter", sans-serif;
}
.font-mono {
font-family: "Roboto Mono", monospace;
}
.font-mono {
font-family: "Roboto Mono", monospace;
}
.p5Canvas {
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.p5Canvas {
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
video {
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
video {
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: #2563eb;
cursor: pointer;
border-radius: 50%;
margin-top: -8px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: #2563eb;
cursor: pointer;
border-radius: 50%;
margin-top: -8px;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #2563eb;
cursor: pointer;
border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #2563eb;
cursor: pointer;
border-radius: 50%;
}
.shadow-up {
box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1),
0 -2px 4px -2px rgb(0 0 0 / 0.1);
}
.shadow-up {
box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1),
0 -2px 4px -2px rgb(0 0 0 / 0.1);
}
#modal-overlay {
transition: opacity 0.2s ease-in-out;
}
#modal-overlay {
transition: opacity 0.2s ease-in-out;
}
#modal-content {
transition: transform 0.2s ease-in-out;
}
</style>
#modal-content {
transition: transform 0.2s ease-in-out;
}
</style>
</head> </head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 flex flex-col min-h-screen"> <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-10 relative">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Radar and Video Visualizer
</h1>
<p class="text-sm text-gray-600 dark:text-gray-400">
High-precision, timestamp-synchronized playback.
</p>
<!-- DARK MODE: Step 2 - Add the toggle button -->
<div class="absolute top-4 right-4">
<button id="theme-toggle" type="button"
class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
<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"
fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</button>
</div>
</header>
<header class="bg-white dark:bg-gray-800 shadow-md p-4 z-10 relative">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Radar and Video Visualizer
</h1>
<p class="text-sm text-gray-600 dark:text-gray-400">
High-precision, timestamp-synchronized playback.
</p>
<main class="flex-grow container mx-auto p-4 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-[60vh] 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 visualization
</p>
</div>
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center justify-center gap-4">
<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"
class="bg-black bg-opacity-60 text-white text-sm px-3 py-1.5 rounded-md hidden font-mono"></div>
</div>
</div>
<div id="feature-toggles"
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border dark:border-gray-700 flex flex-col items-center gap-4 hidden">
<div class="flex flex-wrap justify-center gap-x-6 gap-y-2">
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-snr-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by SNR</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-cluster-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Cluster</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-inlier-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Inlier</label>
<!-- ADD THIS LINE -->
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-stationary-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Stationary</label>
<!-- END OF ADDED LINE -->
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-velocity" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
checked />
Show Object Details</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-tracks" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
checked />
Show Tracks</label>
<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>
<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"
id="toggle-debug2-overlay"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Show Advanced Debug</label>
<!-- DARK MODE: Step 2 - Add the toggle button -->
<div class="absolute top-4 right-4">
<button id="theme-toggle" type="button"
class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
<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"
fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</button>
</div>
</header>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-close-up"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
CLOSE-UP</label>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<label for="snr-min-input" class="text-sm font-medium">Min SNR:</label><input type="number"
id="snr-min-input" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 dark:text-white rounded-lg text-sm" />
</div>
<div class="flex items-center gap-2">
<label for="snr-max-input" class="text-sm font-medium">Max SNR:</label><input type="number"
id="snr-max-input" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 dark:text-white rounded-lg text-sm" />
</div>
<button id="apply-snr-btn"
class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors text-sm font-medium">
Apply
</button>
</div>
</div>
<main class="flex-grow container mx-auto p-4 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-[60vh] 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 visualization
</p>
</div> </div>
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="w-full h-[45vh] bg-black rounded-lg shadow-inner flex items-center justify-center relative">
<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>
<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>
</div>
<div id="speed-graph-container"
class="w-full h-[27vh] 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 CAN log to see speed graph
</p>
</div>
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center justify-center gap-4">
<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"
class="bg-black bg-opacity-60 text-white text-sm px-3 py-1.5 rounded-md hidden font-mono"></div>
</div> </div>
</main>
</div>
<div id="feature-toggles"
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border dark:border-gray-700 flex flex-col items-center gap-4 hidden">
<div class="flex flex-wrap justify-center gap-x-6 gap-y-2">
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-snr-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by SNR</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-cluster-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Cluster</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-inlier-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Inlier</label>
<!-- ADD THIS LINE -->
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-stationary-color" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Stationary</label>
<!-- END OF ADDED LINE -->
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-velocity"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked />
Show Object Details</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-tracks"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked />
Show Tracks</label>
<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>
<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"
id="toggle-debug2-overlay" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Show Advanced Debug</label>
<footer class="bg-white dark:bg-gray-800 shadow-up w-full p-4 mt-auto sticky bottom-0 z-20">
<div class="mb-4">
<input type="range" id="timeline-slider" min="0" max="0" value="0"
class="w-full h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer" />
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-close-up"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
CLOSE-UP</label>
</div> </div>
<div class="flex flex-wrap items-center justify-center md:justify-between gap-4">
<div class="flex items-center gap-4 justify-center">
<button id="load-json-btn"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
Load JSON
</button>
<button id="load-video-btn"
class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors text-sm font-medium">
Load Video
</button>
<button id="load-can-btn"
class="bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600 transition-colors text-sm font-medium">
Load CAN Log
</button>
<button id="clear-cache-btn"
class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors text-sm font-medium">
Clear Cache
</button>
<div class="flex items-center gap-2">
<label for="offset-input" class="text-sm font-medium">
Offset (ms):<br /><small>(+ve values if radar<br />
lags behind video)</small></label>
<input type="number" id="offset-input" value="0"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 dark:text-white rounded-lg text-sm" />
<span id="auto-offset-indicator" class="text-green-600 font-semibold text-sm hidden">(auto)</span>
</div>
</div>
<div class="flex items-center justify-center gap-4">
<button id="play-pause-btn"
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold w-20">
Play
</button>
<button id="stop-btn"
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold">
Stop
</button>
<div class="text-center">
<span id="frame-counter" class="font-mono text-lg">Frame: 0 / 0</span>
</div>
</div>
<div class="flex items-center justify-center gap-2">
<label for="speed-slider" class="text-sm font-medium">Speed:</label>
<input type="range" id="speed-slider" min="0.1" max="2" value="1" step="0.1"
class="w-32 h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer" />
<span id="speed-display" class="font-mono text-sm w-12 text-center">1.0x</span>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<label for="snr-min-input" class="text-sm font-medium">Min SNR:</label><input type="number"
id="snr-min-input" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 dark:text-white rounded-lg text-sm" />
</div>
<div class="flex items-center gap-2">
<label for="snr-max-input" class="text-sm font-medium">Max SNR:</label><input type="number"
id="snr-max-input" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 dark:text-white rounded-lg text-sm" />
</div>
<button id="apply-snr-btn"
class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors text-sm font-medium">
Apply
</button>
</div> </div>
</footer>
</div>
</div>
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="w-full h-[45vh] bg-black rounded-lg shadow-inner flex items-center justify-center relative">
<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>
<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>
</div>
<div id="speed-graph-container"
class="w-full h-[27vh] 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 CAN log to see speed graph
</p>
</div>
</div>
</main>
<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"
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md z-10 transform scale-95">
<p id="modal-text" class="text-gray-700 dark:text-gray-300 mb-4"></p>
<div id="modal-buttons" class="flex justify-end gap-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><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>
<footer class="bg-white dark:bg-gray-800 shadow-up w-full p-4 mt-auto sticky bottom-0 z-20">
<div class="mb-4">
<input type="range" id="timeline-slider" min="0" max="0" value="0"
class="w-full h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer" />
</div>
<div class="flex flex-wrap items-center justify-center md:justify-between gap-4">
<div class="flex items-center gap-4 justify-center">
<button id="load-json-btn"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
Load JSON
</button>
<button id="load-video-btn"
class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors text-sm font-medium">
Load Video
</button>
<button id="load-can-btn"
class="bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600 transition-colors text-sm font-medium">
Load CAN Log
</button>
<button id="clear-cache-btn"
class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors text-sm font-medium">
Clear Cache
</button>
<div class="flex items-center gap-2">
<label for="offset-input" class="text-sm font-medium">
Offset (ms):<br /><small>(+ve values if radar<br />
lags behind video)</small></label>
<input type="number" id="offset-input" value="0"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 dark:text-white rounded-lg text-sm" />
<span id="auto-offset-indicator" class="text-green-600 font-semibold text-sm hidden">(auto)</span>
</div> </div>
</div>
<div class="flex items-center justify-center gap-4">
<button id="play-pause-btn"
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold w-20">
Play
</button>
<button id="stop-btn"
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold">
Stop
</button>
<div class="text-center">
<span id="frame-counter" class="font-mono text-lg">Frame: 0 / 0</span>
</div>
</div>
<div class="flex items-center justify-center gap-2">
<label for="speed-slider" class="text-sm font-medium">Speed:</label>
<input type="range" id="speed-slider" min="0.1" max="2" value="1" step="0.1"
class="w-32 h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer" />
<span id="speed-display" class="font-mono text-sm w-12 text-center">1.0x</span>
</div>
</div>
</footer>
<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"
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md z-10 transform scale-95">
<p id="modal-text" class="text-gray-700 dark:text-gray-300 mb-4"></p>
<div id="modal-buttons" class="flex justify-end gap-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><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>
</div> </div>
<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="can-file-input" class="hidden" accept=".log, .txt" />
</div>
<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="can-file-input" class="hidden" accept=".log, .txt" />
<script type="module" src="./src/main.js"></script>
<script type="module" src="./src/main.js"></script>
</body> </body>
</html> </html>

43
steps/readme.md

@ -2,9 +2,9 @@ Radar and Video Timestamp VisualizerThis is a high-precision, browser-based tool
Features: Features:
*Synchronized Playback: Simultaneously plays a video file and visualizes radar data frames based on precise timestamps.
\*Synchronized Playback: Simultaneously plays a video file and visualizes radar data frames based on precise timestamps.
*Multi-File Support: Load and visualize data from three distinct sources:
\*Multi-File Support: Load and visualize data from three distinct sources:
-JSON: Contains radar point clouds and tracked object data. -JSON: Contains radar point clouds and tracked object data.
-Video: The ground-truth video corresponding to the radar data. -Video: The ground-truth video corresponding to the radar data.
-CAN Log: A text log file containing vehicle speed data over time. -CAN Log: A text log file containing vehicle speed data over time.
@ -18,12 +18,11 @@ Features:
-Distinguish between stationary and moving objects with unique colors and markers. -Distinguish between stationary and moving objects with unique colors and markers.
-Adjustable SNR range for fine-tuning the visualization. -Adjustable SNR range for fine-tuning the visualization.
*Playback Controls: Full control over the playback, including play, pause, stop, frame-by-frame stepping (using arrow keys), and a draggable timeline slider.
\*Playback Controls: Full control over the playback, including play, pause, stop, frame-by-frame stepping (using arrow keys), and a draggable timeline slider.
*Data Caching: Uses IndexedDB to cache loaded files, allowing for instant reloading of the last session.
*Dark/Light Theme: A theme toggle for user comfort that persists across sessions.
\*Data Caching: Uses IndexedDB to cache loaded files, allowing for instant reloading of the last session.
\*Dark/Light Theme: A theme toggle for user comfort that persists across sessions.
How to Run Locally:- How to Run Locally:-
@ -41,29 +40,29 @@ You must serve the files using a local web server.The easiest way to do this is
If you don't have serve, install it first: If you don't have serve, install it first:
" npm install -g serve.serve ". " npm install -g serve.serve ".
3.) Open in Browser: Open your web browser and navigate to the URL provided by the server (usually http://localhost:8000 or http://localhost:3000).
3.) Open in Browser: Open your web browser and navigate to the URL provided by the server (usually http://localhost:8000 or http://localhost:3000). # Navigate to the server URL
Project StructureThe project has been refactored into a modular structure to separate concerns. All JavaScript source code resides in the src/ directory.. Project StructureThe project has been refactored into a modular structure to separate concerns. All JavaScript source code resides in the src/ directory..
├── index.html # The main HTML shell for the application
├── README.md # This documentation file
├── index.html # The main HTML shell for the application
├── README.md # This documentation file
└── src/ └── src/
├── constants.js # Shared constants (e.g., radar bounds, FPS)
├── db.js # IndexedDB logic for caching files
├── dom.js # DOM element references and UI update functions
├── drawUtils.js # p5.js drawing helper functions for the radar sketch
├── fileParsers.js # Logic for parsing JSON and CAN log files
├── main.js # The main application entry point and event wiring
├── modal.js # Logic for the pop-up modal dialog
├── state.js # Centralized application state management
├── sync.js # The core animation loop and playback synchronization logic
├── theme.js # Dark/Light mode theme switching logic
├── utils.js # General utility functions (e.g., binary search, throttling)
├── constants.js # Shared constants (e.g., radar bounds, FPS)
├── db.js # IndexedDB logic for caching files
├── dom.js # DOM element references and UI update functions
├── drawUtils.js # p5.js drawing helper functions for the radar sketch
├── fileParsers.js # Logic for parsing JSON and CAN log files
├── main.js # The main application entry point and event wiring
├── modal.js # Logic for the pop-up modal dialog
├── state.js # Centralized application state management
├── sync.js # The core animation loop and playback synchronization logic
├── theme.js # Dark/Light mode theme switching logic
├── utils.js # General utility functions (e.g., binary search, throttling)
└── p5/ └── p5/
├── radarSketch.js # The p5.js sketch for the main radar visualization
├── radarSketch.js # The p5.js sketch for the main radar visualization
└── speedGraphSketch.js # The p5.js sketch for the speed graph └── speedGraphSketch.js # The p5.js sketch for the speed graph
How to Use the Application How to Use the Application
- Load Files: Use the "Load JSON", "Load Video", and "Load CAN Log" buttons to select your data files. The application works best when all three are loaded. The application will automatically attempt to calculate the time offset between the JSON and video files based on their filenames. - Load Files: Use the "Load JSON", "Load Video", and "Load CAN Log" buttons to select your data files. The application works best when all three are loaded. The application will automatically attempt to calculate the time offset between the JSON and video files based on their filenames.
- Playback: Use the "Play/Pause" and "Stop" buttons to control the timeline. You can also click and drag the main timeline slider or use the Left and Right arrow keys to step through frames. - Playback: Use the "Play/Pause" and "Stop" buttons to control the timeline. You can also click and drag the main timeline slider or use the Left and Right arrow keys to step through frames.
- Adjust Speed: Use the "Speed" slider to change the playback rate of the video and visualization. - Adjust Speed: Use the "Speed" slider to change the playback rate of the video and visualization.

9
steps/src/constants.js

@ -1,7 +1,12 @@
// Maximum number of points to store for each object's trajectory.
export const MAX_TRAJECTORY_LENGTH = 50; export const MAX_TRAJECTORY_LENGTH = 50;
// Frames per second for the video playback.
export const VIDEO_FPS = 30; export const VIDEO_FPS = 30;
// Minimum X-coordinate for the radar plot in meters.
export const RADAR_X_MIN = -20; export const RADAR_X_MIN = -20;
// Maximum X-coordinate for the radar plot in meters.
export const RADAR_X_MAX = 20; export const RADAR_X_MAX = 20;
// Minimum Y-coordinate for the radar plot in meters.
export const RADAR_Y_MIN = 0; export const RADAR_Y_MIN = 0;
export const RADAR_Y_MAX = 60;
// Maximum Y-coordinate for the radar plot in meters.
export const RADAR_Y_MAX = 60;

108
steps/src/db.js

@ -1,55 +1,79 @@
// --- IndexedDB for Caching --- //
// -------------------------- IndexedDB for Caching ----------------- //
let db; let db;
//---Initialize DB---//
//---------------------------Initialize DB----------------------------//
// Initializes the IndexedDB database.
// @param {function} callback - A function to be called once the database is initialized.
export function initDB(callback) { export function initDB(callback) {
const request = indexedDB.open('visualizerDB', 1);
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains('files')) {
db.createObjectStore('files');
}
};
request.onsuccess = function (event) {
db = event.target.result;
console.log("Database initialized");
if (callback) callback();
}; request.onerror = function (event) {
console.error("IndexedDB error:", event.target.errorCode);
};
// Open the database with the name "visualizerDB" and version 1.
const request = indexedDB.open("visualizerDB", 1);
// Event handler for when the database needs to be upgraded (e.g., first time creation or version change).
request.onupgradeneeded = function (event) {
const db = event.target.result;
// Create an object store named "files" if it doesn't already exist.
if (!db.objectStoreNames.contains("files")) {
db.createObjectStore("files");
}
};
// Event handler for a successful database opening.
request.onsuccess = function (event) {
db = event.target.result;
console.log("Database initialized");
// Call the provided callback function.
if (callback) callback();
};
// Event handler for an error during database opening.
request.onerror = function (event) {
console.error("IndexedDB error:", event.target.errorCode);
};
} }
//---save file---//
//---------------------------save file------------------------------//
// Saves a file (or any value) to the IndexedDB.
// @param {string} key - The key to store the value under.
// @param {*} value - The value to be stored.
export function saveFileToDB(key, value) { export function saveFileToDB(key, value) {
if (!db) return;
const transaction = db.transaction(['files'], 'readwrite');
const store = transaction.objectStore('files');
const request = store.put(value, key);
request.onsuccess = () => console.log(`File '${key}' saved to DB.`);
request.onerror = (event) => console.error(`Error saving file '${key}':`, event.target.error);
// If the database is not initialized, return.
if (!db) return;
// Start a read-write transaction on the "files" object store.
const transaction = db.transaction(["files"], "readwrite");
const store = transaction.objectStore("files");
// Put (add or update) the value with the given key.
const request = store.put(value, key);
// Event handler for a successful save operation.
request.onsuccess = () => console.log(`File '${key}' saved to DB.`);
// Event handler for an error during saving.
request.onerror = (event) =>
console.error(`Error saving file '${key}':`, event.target.error);
} }
//---load file---//
//---------------------------load file--------------------------------//
export function loadFileFromDB(key, callback) { export function loadFileFromDB(key, callback) {
if (!db) return;
const transaction = db.transaction(['files'], 'readonly');
const store = transaction.objectStore('files'); const request = store.get(key);
request.onsuccess = function () {
if (request.result) {
callback(request.result);
}
else {
console.log(`File '${key}' not found in DB.`);
callback(null);
}
};
request.onerror = (event) => {
console.error(`Error loading file '${key}':`, event.target.error);
callback(null);
};
// If the database is not initialized, return.
if (!db) return;
// Start a read-only transaction on the "files" object store.
const transaction = db.transaction(["files"], "readonly");
const store = transaction.objectStore("files");
// Get the value associated with the given key.
const request = store.get(key);
// Event handler for a successful retrieval.
request.onsuccess = function () {
// If a result is found, call the callback with the result.
if (request.result) {
callback(request.result);
} else {
console.log(`File '${key}' not found in DB.`);
callback(null);
}
}; // Event handler for an error during loading.
request.onerror = (event) => {
console.error(`Error loading file '${key}':`, event.target.error);
callback(null);
};
} }

68
steps/src/dom.js

@ -1,6 +1,6 @@
import { appState } from "./state.js"; import { appState } from "./state.js";
import { findLastCanIndexBefore } from "./utils.js"; import { findLastCanIndexBefore } from "./utils.js";
import { VIDEO_FPS } from "./constants.js";
import { VIDEO_FPS } from "./constants.js"; // Import VIDEO_FPS for debug overlay calculations
// --- DOM Element References --- // // --- DOM Element References --- //
@ -65,28 +65,26 @@ export const modalCancelBtn = document.getElementById("modal-cancel-btn");
export const toggleCloseUp = document.getElementById("toggle-close-up"); export const toggleCloseUp = document.getElementById("toggle-close-up");
//----------------------UPDATE FRAME Function----------------------// //----------------------UPDATE FRAME Function----------------------//
// Located in: src/dom.js
// Updates the UI to reflect the current radar frame and synchronizes video playback.
export function updateFrame(frame, forceVideoSeek) { export function updateFrame(frame, forceVideoSeek) {
if ( if (
!appState.vizData || !appState.vizData ||
frame < 0 || frame < 0 ||
frame >= appState.vizData.radarFrames.length frame >= appState.vizData.radarFrames.length
)
return;
) // Exit if no visualization data or invalid frame.
return; // Exit if no visualization data or invalid frame
appState.currentFrame = frame; appState.currentFrame = frame;
timelineSlider.value = appState.currentFrame; timelineSlider.value = appState.currentFrame;
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${ frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${
appState.vizData.radarFrames.length appState.vizData.radarFrames.length
}`; }`;
const frameData = appState.vizData.radarFrames[appState.currentFrame]; const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (toggleEgoSpeed.checked && frameData) {
const egoVy_kmh = (frameData.egoVelocity[1] * 3.6).toFixed(1);
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`; egoSpeedDisplay.textContent = `Ego: ${egoVy_kmh} km/h`;
egoSpeedDisplay.classList.remove("hidden"); egoSpeedDisplay.classList.remove("hidden");
} else { } else {
egoSpeedDisplay.classList.add("hidden");
egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display.
} }
// --- Start of fix --- // --- Start of fix ---
@ -102,14 +100,14 @@ export function updateFrame(frame, forceVideoSeek) {
const offsetMs = parseFloat(offsetInput.value) || 0; const offsetMs = parseFloat(offsetInput.value) || 0;
const targetRadarTimeMs = frameData.timestampMs; const targetRadarTimeMs = frameData.timestampMs;
const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000; const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000;
if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) {
if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) {
videoPlayer.currentTime = targetVideoTimeSec;
if (targetVideoTimeSec >= 0 && 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 // MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime
timeForUpdates = targetVideoTimeSec;
timeForUpdates = targetVideoTimeSec; // Update time for subsequent UI updates
} }
}
} // End of forceVideoSeek block
if (!appState.isPlaying) { if (!appState.isPlaying) {
// MODIFIED: Use our new synchronized time variable // MODIFIED: Use our new synchronized time variable
@ -118,23 +116,23 @@ export function updateFrame(frame, forceVideoSeek) {
} }
// --- End of fix --- // --- End of fix ---
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance && !appState.isPlaying)
if (appState.p5_instance) appState.p5_instance.redraw(); // Redraw radar sketch
if (appState.speedGraphInstance && !appState.isPlaying) // Redraw speed graph if not playing.
appState.speedGraphInstance.redraw(); appState.speedGraphInstance.redraw();
} }
//----------------------RESET VISUALIZATION Function----------------------// //----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state.
export function resetVisualization() { export function resetVisualization() {
appState.isPlaying = false; appState.isPlaying = false;
playPauseBtn.textContent = "Play"; playPauseBtn.textContent = "Play";
const numFrames = appState.vizData.radarFrames.length; const numFrames = appState.vizData.radarFrames.length;
timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0; timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0;
updateFrame(0, true);
updateFrame(0, true); // Update to the first frame and force video seek
} }
//----------------------CAN DISPLAY UPDATE Function----------------------// //----------------------CAN DISPLAY UPDATE Function----------------------//
// Updates the CAN speed display based on the current media time.
export function updateCanDisplay(currentMediaTime) { export function updateCanDisplay(currentMediaTime) {
if ( if (
appState.canData.length > 0 && appState.canData.length > 0 &&
@ -148,19 +146,19 @@ export function updateCanDisplay(currentMediaTime) {
appState.canData appState.canData
); );
if (canIndex !== -1) { if (canIndex !== -1) {
const currentCanMessage = appState.canData[canIndex];
canSpeedDisplay.textContent = `CAN: ${currentCanMessage.speed} km/h`;
const currentCanMessage = appState.canData[canIndex]; // Get the CAN message at the found index
canSpeedDisplay.textContent = `CAN: ${currentCanMessage.speed} km/h`; // Display CAN speed
canSpeedDisplay.classList.remove("hidden"); canSpeedDisplay.classList.remove("hidden");
} else { } else {
canSpeedDisplay.classList.add("hidden");
canSpeedDisplay.classList.add("hidden"); // Hide CAN speed display
} }
} else { } else {
canSpeedDisplay.classList.add("hidden");
canSpeedDisplay.classList.add("hidden"); // Hide CAN speed display.
} }
} }
//----------------------DEBUG OVERLAY UPDATE Function----------------------// //----------------------DEBUG OVERLAY UPDATE Function----------------------//
// Updates the debug overlay with various synchronization and time information.
export function updateDebugOverlay(currentMediaTime) { export function updateDebugOverlay(currentMediaTime) {
// Check the state of both debug toggles // Check the state of both debug toggles
const isDebug1Visible = toggleDebugOverlay.checked; const isDebug1Visible = toggleDebugOverlay.checked;
@ -168,11 +166,11 @@ export function updateDebugOverlay(currentMediaTime) {
// If neither is checked, hide the overlay and stop // If neither is checked, hide the overlay and stop
if (!isDebug1Visible && !isDebug2Visible) { if (!isDebug1Visible && !isDebug2Visible) {
debugOverlay.classList.add("hidden");
debugOverlay.classList.add("hidden"); // Hide debug overlay
return; return;
} }
debugOverlay.classList.remove("hidden");
// If at least one is checked, show the overlay
debugOverlay.classList.remove("hidden"); // Show debug overlay.
let content = []; let content = [];
// --- Logic for the original debug overlay --- // --- Logic for the original debug overlay ---
@ -188,9 +186,9 @@ export function updateDebugOverlay(currentMediaTime) {
.toISOString() .toISOString()
.split("T")[1] .split("T")[1]
.replace("Z", "")}` .replace("Z", "")}`
);
); // Format and display video absolute time
} else { } else {
content.push("Video not loaded...");
content.push("Video not loaded..."); // Indicate video not loaded.
} }
if ( if (
appState.vizData && appState.vizData &&
@ -206,7 +204,7 @@ export function updateDebugOverlay(currentMediaTime) {
.toISOString() .toISOString()
.split("T")[1] .split("T")[1]
.replace("Z", "")}` .replace("Z", "")}`
);
); // Format and display radar absolute time
} }
} }
@ -223,10 +221,10 @@ export function updateDebugOverlay(currentMediaTime) {
const targetRadarTimeMs = currentRadarFrame.timestampMs; const targetRadarTimeMs = currentRadarFrame.timestampMs;
const driftMs = currentMediaTime * 1000 - targetRadarTimeMs; const driftMs = currentMediaTime * 1000 - targetRadarTimeMs;
// Style the drift value to be green if sync is good, and red if it's off
// 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) > 40 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
content.push(`Video Time (s): ${currentMediaTime.toFixed(3)}`);
content.push(`Video Time (s): ${currentMediaTime.toFixed(3)}`); // Display current video time
content.push(`Target Radar Time (ms): ${targetRadarTimeMs.toFixed(0)}`); content.push(`Target Radar Time (ms): ${targetRadarTimeMs.toFixed(0)}`);
content.push( content.push(
`Drift (ms): <b style="color: ${driftColor};">${driftMs.toFixed(0)}</b>` `Drift (ms): <b style="color: ${driftColor};">${driftMs.toFixed(0)}</b>`
@ -237,11 +235,11 @@ export function updateDebugOverlay(currentMediaTime) {
content.push( content.push(
`Radar Start Time: ${new Date(appState.radarStartTimeMs).toISOString()}` `Radar Start Time: ${new Date(appState.radarStartTimeMs).toISOString()}`
); );
content.push(`Calculated Offset (ms): ${offsetInput.value}`);
content.push(`Calculated Offset (ms): ${offsetInput.value}`); // Display calculated offset.
} else { } else {
content.push("Load video and radar data to see sync info.");
content.push("Load video and radar data to see sync info."); // Prompt to load data.
} }
} }
debugOverlay.innerHTML = content.join("<br>");
debugOverlay.innerHTML = content.join("<br>"); // Update debug overlay content.
} }

721
steps/src/drawUtils.js

@ -1,38 +1,44 @@
import { import {
RADAR_X_MAX,
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
MAX_TRAJECTORY_LENGTH
} from './constants.js';
RADAR_X_MAX,
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
MAX_TRAJECTORY_LENGTH,
} from "./constants.js";
import { appState } from "./state.js";
import { import {
appState
} from './state.js';
import {
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleFrameNorm,
toggleVelocity,
toggleStationaryColor
} from './dom.js';
// Color definitions moved from the sketch
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleFrameNorm,
toggleVelocity,
toggleStationaryColor,
} from "./dom.js";
// Defines a set of SNR (Signal-to-Noise Ratio) colors.
export const snrColors = (p) => ({ export const snrColors = (p) => ({
c1: p.color(0, 0, 255),
c2: p.color(0, 255, 255),
c3: p.color(0, 255, 0),
c4: p.color(255, 255, 0),
c5: p.color(255, 0, 0)
c1: p.color(0, 0, 255), // Blue
c2: p.color(0, 255, 255), // Cyan
c3: p.color(0, 255, 0), // Green
c4: p.color(255, 255, 0), // Yellow
c5: p.color(255, 0, 0), // Red
}); });
// Defines a palette of colors for different clusters.
export const clusterColors = (p) => [ export const clusterColors = (p) => [
p.color(230, 25, 75), p.color(60, 180, 75), p.color(0, 130, 200),
p.color(245, 130, 48), p.color(145, 30, 180), p.color(70, 240, 240),
p.color(240, 50, 230), p.color(210, 245, 60), p.color(128, 0, 0),
p.color(0, 128, 128)
p.color(230, 25, 75), // Red
p.color(60, 180, 75), // Green
p.color(0, 130, 200), // Blue
p.color(245, 130, 48), // Orange
p.color(145, 30, 180), // Purple
p.color(70, 240, 240), // Cyan
p.color(240, 50, 230), // Magenta
p.color(210, 245, 60), // Lime Green
p.color(128, 0, 0), // Maroon
p.color(0, 128, 128), // Teal
]; ];
// Defines colors for stationary and moving objects.
export const stationaryColor = (p) => p.color(218, 165, 32); // Goldenrod export const stationaryColor = (p) => p.color(218, 165, 32); // Goldenrod
export const movingColor = (p) => p.color(255, 0, 255); // Magenta export const movingColor = (p) => p.color(255, 0, 255); // Magenta
@ -42,20 +48,38 @@ export const movingColor = (p) => p.color(255, 0, 255); // Magenta
* @param {object} plotScales - The calculated scales for plotting. * @param {object} plotScales - The calculated scales for plotting.
*/ */
export function drawStaticRegionsToBuffer(p, b, plotScales) { export function drawStaticRegionsToBuffer(p, b, plotScales) {
b.clear();
b.push();
b.translate(b.width / 2, b.height * 0.95);
b.scale(1, -1);
b.stroke(100, 100, 100, 150);
b.strokeWeight(1);
b.drawingContext.setLineDash([8, 8]);
const a1 = p.radians(30),
a2 = p.radians(150);
const len = 70;
b.line(0, 0, len * p.cos(a1) * plotScales.plotScaleX, len * p.sin(a1) * plotScales.plotScaleY);
b.line(0, 0, len * p.cos(a2) * plotScales.plotScaleX, len * p.sin(a2) * plotScales.plotScaleY);
b.drawingContext.setLineDash([]);
b.pop();
b.clear();
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.
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 len = 70;
// Draw the first static region line.
b.line(
0,
0,
len * p.cos(a1) * plotScales.plotScaleX,
len * p.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
);
// Reset line dash pattern.
b.drawingContext.setLineDash([]);
b.pop();
} }
/** /**
@ -64,40 +88,73 @@ export function drawStaticRegionsToBuffer(p, b, plotScales) {
* @param {object} plotScales - The calculated scales for plotting. * @param {object} plotScales - The calculated scales for plotting.
*/ */
export function drawAxes(p, plotScales) { export function drawAxes(p, plotScales) {
p.push();
// Determine axis and text colors based on the current theme (dark/light mode).
const axisColor = document.documentElement.classList.contains("dark")
? p.color(100)
: p.color(220);
const mainAxisColor = document.documentElement.classList.contains("dark")
? p.color(150)
: p.color(180);
const textColor = document.documentElement.classList.contains("dark")
? p.color(200)
: p.color(150);
// Draw horizontal grid lines.
p.stroke(axisColor);
p.strokeWeight(1);
for (let y = 5; y <= RADAR_Y_MAX; y += 5)
p.line(
RADAR_X_MIN * plotScales.plotScaleX,
y * plotScales.plotScaleY,
RADAR_X_MAX * plotScales.plotScaleX,
y * plotScales.plotScaleY
);
// Draw vertical grid lines.
for (let x = -15; x <= 15; x += 5) {
if (x === 0) continue;
p.line(
x * plotScales.plotScaleX,
RADAR_Y_MIN * plotScales.plotScaleY,
x * plotScales.plotScaleX,
RADAR_Y_MAX * plotScales.plotScaleY
);
}
p.stroke(mainAxisColor);
p.line(
RADAR_X_MIN * plotScales.plotScaleX,
0,
RADAR_X_MAX * plotScales.plotScaleX,
0
);
p.line(
0,
RADAR_Y_MIN * plotScales.plotScaleY,
0,
RADAR_Y_MAX * plotScales.plotScaleY
);
// Draw Y-axis labels.
p.fill(textColor);
p.noStroke();
p.textSize(10);
for (let y = 5; y <= RADAR_Y_MAX; y += 5) {
p.push(); p.push();
const axisColor = document.documentElement.classList.contains('dark') ? p.color(100) : p.color(220);
const mainAxisColor = document.documentElement.classList.contains('dark') ? p.color(150) : p.color(180);
const textColor = document.documentElement.classList.contains('dark') ? p.color(200) : p.color(150);
p.stroke(axisColor);
p.strokeWeight(1);
for (let y = 5; y <= RADAR_Y_MAX; y += 5) p.line(RADAR_X_MIN * plotScales.plotScaleX, y * plotScales.plotScaleY, RADAR_X_MAX * plotScales.plotScaleX, y * plotScales.plotScaleY);
for (let x = -15; x <= 15; x += 5) {
if (x === 0) continue;
p.line(x * plotScales.plotScaleX, RADAR_Y_MIN * plotScales.plotScaleY, x * plotScales.plotScaleX, RADAR_Y_MAX * plotScales.plotScaleY);
}
p.stroke(mainAxisColor);
p.line(RADAR_X_MIN * plotScales.plotScaleX, 0, RADAR_X_MAX * plotScales.plotScaleX, 0);
p.line(0, RADAR_Y_MIN * plotScales.plotScaleY, 0, RADAR_Y_MAX * plotScales.plotScaleY);
p.fill(textColor);
p.noStroke();
p.textSize(10);
for (let y = 5; y <= RADAR_Y_MAX; y += 5) {
p.push();
p.translate(5, y * plotScales.plotScaleY);
p.scale(1, -1);
p.text(y, 0, 4);
p.pop();
}
for (let x = -15; x <= 15; x += 5) {
if (x === 0) continue;
p.push();
p.translate(x * plotScales.plotScaleX, -10);
p.scale(1, -1);
p.textAlign(p.CENTER);
p.text(x, 0, 0);
p.pop();
}
p.translate(5, y * plotScales.plotScaleY);
// Flip text vertically to align with flipped Y-axis.
p.scale(1, -1);
p.text(y, 0, 4);
p.pop(); p.pop();
}
// Draw X-axis labels.
for (let x = -15; x <= 15; x += 5) {
if (x === 0) continue;
p.push();
p.translate(x * plotScales.plotScaleX, -10);
p.scale(1, -1);
p.textAlign(p.CENTER);
p.text(x, 0, 0);
p.pop();
}
p.pop();
} }
/** /**
@ -107,50 +164,88 @@ export function drawAxes(p, plotScales) {
* @param {object} plotScales - The calculated scales for plotting. * @param {object} plotScales - The calculated scales for plotting.
*/ */
export function drawPointCloud(p, points, plotScales) { export function drawPointCloud(p, points, plotScales) {
p.strokeWeight(4);
const useSnr = toggleSnrColor.checked;
const useCluster = toggleClusterColor.checked;
const useInlier = toggleInlierColor.checked;
const useFrameNorm = toggleFrameNorm.checked;
let minSnr = appState.globalMinSnr,
maxSnr = appState.globalMaxSnr;
if (useSnr && useFrameNorm && points.length > 0) {
const snrVals = points.map(p => p.snr).filter(snr => snr !== null);
if (snrVals.length > 1) {
minSnr = Math.min(...snrVals);
maxSnr = Math.max(...snrVals);
} else if (snrVals.length === 1) {
minSnr = snrVals[0] - 1;
maxSnr = snrVals[0] + 1;
}
// Set stroke weight for points.
p.strokeWeight(4);
// Get state of various toggles from the DOM.
const useSnr = toggleSnrColor.checked;
const useCluster = toggleClusterColor.checked;
const useInlier = toggleInlierColor.checked;
const useFrameNorm = toggleFrameNorm.checked;
let minSnr = appState.globalMinSnr, // Initialize with global SNR range.
maxSnr = appState.globalMaxSnr;
if (useSnr && useFrameNorm && points.length > 0) {
const snrVals = points.map((p) => p.snr).filter((snr) => snr !== null);
if (snrVals.length > 1) {
minSnr = Math.min(...snrVals);
maxSnr = Math.max(...snrVals);
} else if (snrVals.length === 1) {
minSnr = snrVals[0] - 1;
maxSnr = snrVals[0] + 1;
} }
// This check is important. The p5_instance might not be fully initialized yet.
if (useSnr && p.drawSnrLegendToBuffer) p.drawSnrLegendToBuffer(minSnr, maxSnr);
const localClusterColors = clusterColors(p);
const localSnrColors = snrColors(p);
for (const pt of points) {
if (pt && pt.x !== null && pt.y !== null) {
if (useCluster && pt.clusterNumber !== null) {
p.stroke(pt.clusterNumber > 0 ? localClusterColors[(pt.clusterNumber - 1) % localClusterColors.length] : 128);
} else if (useInlier) {
p.stroke(pt.isOutlier === false ? p.color(0, 255, 0) : pt.isOutlier === true ? p.color(255, 0, 0) : 128);
} else if (useSnr && pt.snr !== null) {
const amt = p.map(pt.snr, minSnr, maxSnr, 0, 1, true);
let c;
if (amt < 0.25) c = p.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
else if (amt < 0.5) c = p.lerpColor(localSnrColors.c2, localSnrColors.c3, (amt - 0.25) / 0.25);
else if (amt < 0.75) c = p.lerpColor(localSnrColors.c3, localSnrColors.c4, (amt - 0.5) / 0.25);
else c = p.lerpColor(localSnrColors.c4, localSnrColors.c5, (amt - 0.75) / 0.25);
p.stroke(c);
} else {
p.stroke(0, 150, 255);
}
p.point(pt.x * plotScales.plotScaleX, pt.y * plotScales.plotScaleY);
}
}
// Draw SNR legend if enabled and p5 instance is ready.
if (useSnr && p.drawSnrLegendToBuffer)
p.drawSnrLegendToBuffer(minSnr, maxSnr);
// Get local color instances for cluster and SNR.
const localClusterColors = clusterColors(p);
const localSnrColors = snrColors(p);
// Iterate through each point in the point cloud.
for (const pt of points) {
if (pt && pt.x !== null && pt.y !== null) {
// Apply cluster coloring if enabled.
if (useCluster && pt.clusterNumber !== null) {
p.stroke(
pt.clusterNumber > 0
? localClusterColors[
(pt.clusterNumber - 1) % localClusterColors.length
]
: 128
// Default to gray if cluster number is 0 or invalid.
);
} else if (useInlier) {
p.stroke(
pt.isOutlier === false
? p.color(0, 255, 0)
: pt.isOutlier === true
? p.color(255, 0, 0)
: 128
// Default to gray if inlier status is unknown.
);
} else if (useSnr && pt.snr !== null) {
const amt = p.map(pt.snr, minSnr, maxSnr, 0, 1, true);
let c;
if (amt < 0.25)
c = p.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
else if (amt < 0.5)
c = p.lerpColor(
localSnrColors.c2,
localSnrColors.c3,
(amt - 0.25) / 0.25
);
else if (amt < 0.75)
c = p.lerpColor(
localSnrColors.c3,
localSnrColors.c4,
(amt - 0.5) / 0.25
);
else
c = p.lerpColor(
localSnrColors.c4,
localSnrColors.c5,
(amt - 0.75) / 0.25
// Interpolate color based on SNR value.
);
p.stroke(c);
// Default point color if no specific coloring is applied.
} else {
p.stroke(0, 150, 255);
}
p.point(pt.x * plotScales.plotScaleX, pt.y * plotScales.plotScaleY);
} }
}
} }
/** /**
@ -159,37 +254,62 @@ export function drawPointCloud(p, points, plotScales) {
* @param {object} plotScales - The calculated scales for plotting. * @param {object} plotScales - The calculated scales for plotting.
*/ */
export function drawTrajectories(p, plotScales) { export function drawTrajectories(p, plotScales) {
for (const track of appState.vizData.tracks) {
const logs = track.historyLog.filter(log => log.frameIdx <= appState.currentFrame + 1);
if (logs.length < 2) continue;
const lastLog = logs[logs.length - 1];
if (appState.currentFrame + 1 - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH) continue;
const isCurrentlyStationary = lastLog.isStationary;
let maxLen = isCurrentlyStationary ? Math.floor(MAX_TRAJECTORY_LENGTH / 4) : MAX_TRAJECTORY_LENGTH;
let trajPts = logs.filter(log => log.correctedPosition && log.correctedPosition[0] !== null).map(log => log.correctedPosition);
if (trajPts.length > maxLen) {
trajPts = trajPts.slice(trajPts.length - maxLen);
}
p.push();
p.noFill();
if (isCurrentlyStationary) {
p.stroke(34, 139, 34, 220); // Forest green
p.strokeWeight(1);
p.drawingContext.setLineDash([3, 3]);
} else {
p.stroke(document.documentElement.classList.contains('dark') ? p.color(10, 170, 255, 250) : p.color(0, 50, 255, 250));
p.strokeWeight(1.5);
}
p.beginShape();
for (const pos of trajPts) p.vertex(pos[0] * plotScales.plotScaleX, pos[1] * plotScales.plotScaleY);
p.endShape();
p.drawingContext.setLineDash([]);
p.pop();
// Iterate through each tracked object.
for (const track of appState.vizData.tracks) {
// Filter history logs to include only frames up to the current one.
const logs = track.historyLog.filter(
(log) => log.frameIdx <= appState.currentFrame + 1
);
// Skip if there are not enough points to draw a trajectory.
if (logs.length < 2) continue;
// Get the last log entry.
const lastLog = logs[logs.length - 1];
// Skip if the trajectory is too old.
if (appState.currentFrame + 1 - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH)
continue;
// Adjust trajectory length based on whether the object is stationary.
const isCurrentlyStationary = lastLog.isStationary;
let maxLen = isCurrentlyStationary
? Math.floor(MAX_TRAJECTORY_LENGTH / 4)
: MAX_TRAJECTORY_LENGTH;
// Filter and map corrected positions for the trajectory.
let trajPts = logs
.filter(
(log) => log.correctedPosition && log.correctedPosition[0] !== null
)
.map((log) => log.correctedPosition);
// Slice the trajectory to the maximum allowed length.
if (trajPts.length > maxLen) {
trajPts = trajPts.slice(trajPts.length - maxLen);
} }
// Begin drawing the trajectory.
p.push();
p.noFill();
if (isCurrentlyStationary) {
p.stroke(34, 139, 34, 220); // Forest green
p.strokeWeight(1);
p.drawingContext.setLineDash([3, 3]);
} else {
// Set color and weight for moving trajectories based on theme.
p.stroke(
document.documentElement.classList.contains("dark")
? p.color(10, 170, 255, 250)
: p.color(0, 50, 255, 250)
);
p.strokeWeight(1.5);
}
// Draw the trajectory as a continuous line.
p.beginShape();
for (const pos of trajPts)
p.vertex(pos[0] * plotScales.plotScaleX, pos[1] * plotScales.plotScaleY);
// End drawing and reset line dash.
p.endShape();
p.drawingContext.setLineDash([]);
p.pop();
}
} }
/** /**
@ -198,65 +318,93 @@ export function drawTrajectories(p, plotScales) {
* @param {object} plotScales - The calculated scales for plotting. * @param {object} plotScales - The calculated scales for plotting.
*/ */
export function drawTrackMarkers(p, plotScales) { export function drawTrackMarkers(p, plotScales) {
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);
for (const track of appState.vizData.tracks) {
const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
if (log) {
const pos = (log.correctedPosition && log.correctedPosition[0] !== null) ? log.correctedPosition : log.predictedPosition;
if (pos && pos.length === 2 && pos[0] !== null && pos[1] !== null) {
const size = 5,
x = pos[0] * plotScales.plotScaleX,
y = pos[1] * plotScales.plotScaleY;
let velocityColor = p.color(255, 0, 255, 200);
p.push();
p.strokeWeight(2);
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();
const showDetails = toggleVelocity.checked;
const useStationary = toggleStationaryColor.checked;
// Determine text color based on theme.
const textColor = document.documentElement.classList.contains("dark")
? p.color(255)
: p.color(0);
// Get local color instances for stationary and moving objects.
const localStationaryColor = stationaryColor(p);
const localMovingColor = movingColor(p);
// Iterate through each tracked object.
for (const track of appState.vizData.tracks) {
// Find the log entry for the current frame.
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame + 1
);
if (log) {
const pos =
log.correctedPosition && log.correctedPosition[0] !== null
? log.correctedPosition // Use corrected position if available.
: log.predictedPosition;
if (pos && pos.length === 2 && pos[0] !== null && pos[1] !== null) {
const size = 5,
x = pos[0] * plotScales.plotScaleX,
y = pos[1] * plotScales.plotScaleY;
let velocityColor = p.color(255, 0, 255, 200);
p.push();
p.strokeWeight(2);
if (useStationary && log.isStationary === true) {
p.stroke(localStationaryColor);
p.noFill();
p.rectMode(p.CENTER);
p.square(x, y, size * 1.5);
velocityColor = localStationaryColor; // Set velocity color to stationary.
} else {
let markerColor = p.color(0, 0, 255);
if (useStationary && log.isStationary === false) {
// If not stationary, use moving color.
markerColor = localMovingColor;
// Set velocity color to moving.
velocityColor = localMovingColor;
}
p.stroke(markerColor);
p.line(x - size, y, x + size, y);
p.line(x, y - size, x, y + size);
}
p.pop();
if (showDetails && log.predictedVelocity && log.predictedVelocity[0] !== null) {
const [vx, vy] = log.predictedVelocity;
if (log.isStationary === false) {
p.push();
p.stroke(velocityColor);
p.strokeWeight(2);
p.line(x, y, (pos[0] + vx) * plotScales.plotScaleX, (pos[1] + vy) * plotScales.plotScaleY);
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}`;
p.push();
p.fill(textColor);
p.noStroke();
p.scale(1, -1);
p.textSize(12);
p.text(text, x + 10, -y);
p.pop();
}
}
// Draw velocity vector and text details if enabled.
if (
showDetails &&
log.predictedVelocity &&
log.predictedVelocity[0] !== null
) {
const [vx, vy] = log.predictedVelocity;
if (log.isStationary === false) {
// Only draw velocity for moving objects.
p.push();
p.stroke(velocityColor);
p.strokeWeight(2);
p.line(
x,
y,
(pos[0] + vx) * plotScales.plotScaleX,
(pos[1] + vy) * plotScales.plotScaleY
);
p.pop();
} // Calculate speed in km/h.
const speed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1);
// Format TTC (Time To Collision) if available and finite.
const ttc =
log.ttc !== null && isFinite(log.ttc) && log.ttc < 100
? `TTC: ${log.ttc.toFixed(1)}s`
: "";
// Construct info text.
const text = `ID: ${track.id} | ${speed} km/h\n${ttc}`;
p.push();
p.fill(textColor);
p.noStroke();
p.scale(1, -1);
p.textSize(12);
p.text(text, x + 10, -y);
p.pop();
} }
}
} }
}
} }
/** /**
@ -265,83 +413,106 @@ export function drawTrackMarkers(p, plotScales) {
* @param {object} plotScales - The calculated scales for plotting. * @param {object} plotScales - The calculated scales for plotting.
*/ */
export function handleCloseUpDisplay(p, plotScales) { export function handleCloseUpDisplay(p, plotScales) {
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData || !frameData.pointCloud) return;
const hoveredPoints = [];
const radius = 10;
for (const pt of frameData.pointCloud) {
if (pt.x === null || pt.y === null) continue;
const screenX = (pt.x * plotScales.plotScaleX) + p.width / 2;
const screenY = p.height * 0.95 - (pt.y * plotScales.plotScaleY);
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
hoveredPoints.push({
point: pt,
screenX: screenX,
screenY: screenY
});
}
// Get current frame data.
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData || !frameData.pointCloud) return;
const hoveredPoints = [];
const radius = 10;
// Iterate through point cloud to find hovered points.
for (const pt of frameData.pointCloud) {
if (pt.x === null || pt.y === null) continue;
// Convert radar coordinates to screen coordinates.
const screenX = pt.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY; // Y-axis is inverted for drawing.
const d = p.dist(p.mouseX, p.mouseY, screenX, screenY);
if (d < radius) {
hoveredPoints.push({
point: pt,
screenX: screenX,
screenY: screenY,
});
} }
}
if (hoveredPoints.length > 0) {
hoveredPoints.sort((a, b) => a.screenY - b.screenY);
p.push();
p.textSize(12);
const lineHeight = 15;
const boxPadding = 8;
let boxWidth = 0;
const infoStrings = [];
for (const hovered of hoveredPoints) {
const pt = hovered.point;
const vel = pt.velocity !== null ? pt.velocity.toFixed(2) : 'N/A';
const snr = pt.snr !== null ? pt.snr.toFixed(1) : 'N/A';
const infoText = `X:${pt.x.toFixed(2)}, Y:${pt.y.toFixed(2)} | V:${vel}, SNR:${snr}`;
infoStrings.push(infoText);
boxWidth = Math.max(boxWidth, p.textWidth(infoText));
}
const boxHeight = (infoStrings.length * lineHeight) + (boxPadding * 2);
boxWidth += (boxPadding * 2);
const xOffset = 20;
let boxX = p.mouseX + xOffset;
let boxY = p.mouseY - (boxHeight / 2);
if (boxX + boxWidth > p.width) {
boxX = p.mouseX - boxWidth - xOffset;
}
boxY = p.constrain(boxY, 0, p.height - boxHeight);
const highlightColor = p.color(46, 204, 113);
for (let i = 0; i < hoveredPoints.length; i++) {
const hovered = hoveredPoints[i];
p.noFill();
p.stroke(highlightColor);
p.strokeWeight(2);
p.ellipse(hovered.screenX, hovered.screenY, 15, 15);
p.strokeWeight(1);
p.line(boxX + boxPadding, boxY + boxPadding + (i * lineHeight) + (lineHeight / 2), hovered.screenX, hovered.screenY);
}
const bgColor = document.documentElement.classList.contains('dark') ? p.color(20, 20, 30, 255) : p.color(245, 245, 245, 255);
p.fill(bgColor);
p.stroke(highlightColor);
p.strokeWeight(1);
p.rect(boxX, boxY, boxWidth, boxHeight, 4);
// If points are hovered, display detailed info.
if (hoveredPoints.length > 0) {
// Sort points by Y-coordinate for consistent display.
hoveredPoints.sort((a, b) => a.screenY - b.screenY);
const textColor = document.documentElement.classList.contains('dark') ? p.color(230) : p.color(20);
p.fill(textColor);
p.noStroke();
p.textAlign(p.LEFT, p.TOP);
for (let i = 0; i < infoStrings.length; i++) {
p.text(infoStrings[i], boxX + boxPadding, boxY + boxPadding + (i * lineHeight));
}
p.push();
p.textSize(12);
const lineHeight = 15; // Line height for text in the info box.
const boxPadding = 8;
let boxWidth = 0;
const infoStrings = [];
for (const hovered of hoveredPoints) {
const pt = hovered.point;
const vel = pt.velocity !== null ? pt.velocity.toFixed(2) : "N/A";
const snr = pt.snr !== null ? pt.snr.toFixed(1) : "N/A";
const infoText = `X:${pt.x.toFixed(2)}, Y:${pt.y.toFixed(
2
)} | V:${vel}, SNR:${snr}`;
infoStrings.push(infoText);
boxWidth = Math.max(boxWidth, p.textWidth(infoText));
} // Calculate box dimensions.
const boxHeight = infoStrings.length * lineHeight + boxPadding * 2;
boxWidth += boxPadding * 2;
// Position the info box relative to the mouse.
const xOffset = 20;
let boxX = p.mouseX + xOffset;
let boxY = p.mouseY - boxHeight / 2;
// Adjust box position to stay within canvas bounds.
if (boxX + boxWidth > p.width) {
boxX = p.mouseX - boxWidth - xOffset;
}
boxY = p.constrain(boxY, 0, p.height - boxHeight);
// Highlight hovered points and draw connecting lines to the info box.
const highlightColor = p.color(46, 204, 113);
for (let i = 0; i < hoveredPoints.length; i++) {
const hovered = hoveredPoints[i];
p.noFill();
p.stroke(highlightColor);
p.strokeWeight(2);
p.ellipse(hovered.screenX, hovered.screenY, 15, 15);
p.strokeWeight(1);
p.line(
boxX + boxPadding,
boxY + boxPadding + i * lineHeight + lineHeight / 2,
hovered.screenX,
hovered.screenY
);
}
p.pop();
// Draw the info box background and border.
const bgColor = document.documentElement.classList.contains("dark")
? p.color(20, 20, 30, 255)
: p.color(245, 245, 245, 255);
p.fill(bgColor);
p.stroke(highlightColor);
p.strokeWeight(1);
p.rect(boxX, boxY, boxWidth, boxHeight, 4);
// Draw the text content inside the info box.
const textColor = document.documentElement.classList.contains("dark")
? p.color(230)
: p.color(20);
p.fill(textColor);
p.noStroke();
p.textAlign(p.LEFT, p.TOP);
for (let i = 0; i < infoStrings.length; i++) {
p.text(
infoStrings[i],
boxX + boxPadding,
boxY + boxPadding + i * lineHeight
);
} }
p.pop();
}
} }

197
steps/src/fileParsers.js

@ -1,88 +1,139 @@
//--------------------CAN-LOG PARSER------------------------// //--------------------CAN-LOG PARSER------------------------//
export function processCanLog(logContent, videoStartDate) { export function processCanLog(logContent, videoStartDate) {
// The function receives everything it needs as arguments.
// It no longer looks at the global state.
if (!videoStartDate) {
// If the video isn't loaded, it can't do its job.
// It returns an object describing the problem.
return { error: "Please load the video file first to synchronize the CAN log.", rawCanLogText: logContent };
}
// This is a NEW, LOCAL variable, only for this function.
const canData = [];
const lines = logContent.split('\n');
const logRegex = /(\d{2}):(\d{2}):(\d{2}):(\d{4})\s+Rx\s+\d+\s+0x([0-9a-fA-F]+)\s+s\s+\d+((?:\s+[0-9a-fA-F]{2})+)/;
const canIdToDecode = '30F';
for (const line of lines) {
const match = line.match(logRegex);
if (match && match[5].toUpperCase() === canIdToDecode) {
const [h, m, s, ms] = [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4].substring(0, 3))];
const msgDate = new Date(videoStartDate);
msgDate.setUTCHours(h, m, s, ms);
const dataBytes = match[6].trim().split(/\s+/).map(hex => parseInt(hex, 16));
if (dataBytes.length >= 2) {
const rawVal = (dataBytes[0] << 3) | (dataBytes[1] >> 5);
const speed = (rawVal * 0.1).toFixed(1);
canData.push({ time: msgDate.getTime(), speed: speed });
}
}
// The function now receives all necessary data (logContent, videoStartDate) as arguments,
// making it a pure function that doesn't rely on global state.
if (!videoStartDate) {
// If videoStartDate is not provided, it means the video file hasn't been loaded yet.
// The CAN log cannot be synchronized without it, so an error is returned.
return {
// Error message to be displayed to the user.
error: "Please load the video file first to synchronize the CAN log.",
// The raw log content is returned so it can be stored and processed later
// once the videoStartDate becomes available.
rawCanLogText: logContent,
};
}
// This is a NEW, LOCAL variable, only for this function.
const canData = [];
const lines = logContent.split("\n");
// Regular expression to parse CAN log lines.
// It captures time components (HH:MM:SS:ms), CAN ID, and data bytes.
const logRegex =
/(\d{2}):(\d{2}):(\d{2}):(\d{4})\s+Rx\s+\d+\s+0x([0-9a-fA-F]+)\s+s\s+\d+((?:\s+[0-9a-fA-F]{2})+)/;
// The specific CAN ID (0x30F) we are interested in for speed data.
const canIdToDecode = "30F";
for (const line of lines) {
const match = line.match(logRegex);
// Check if the line matches the regex and if the CAN ID is the one we want.
if (match && match[5].toUpperCase() === canIdToDecode) {
// Extract time components from the regex match.
const [h, m, s, ms] = [
parseInt(match[1]),
parseInt(match[2]),
parseInt(match[3]),
parseInt(match[4].substring(0, 3)),
];
// Create a Date object for the CAN message timestamp.
// It uses the video's start date and then sets the time components from the log.
const msgDate = new Date(videoStartDate);
msgDate.setUTCHours(h, m, s, ms);
// Extract and parse data bytes from the regex match.
const dataBytes = match[6]
.trim()
.split(/\s+/)
.map((hex) => parseInt(hex, 16));
// Check if there are enough data bytes to extract speed information.
if (dataBytes.length >= 2) {
// Decode the raw speed value from the first two data bytes.
// This specific decoding logic is based on the CAN message format.
const rawVal = (dataBytes[0] << 3) | (dataBytes[1] >> 5);
// Convert the raw value to km/h and format it to one decimal place.
const speed = (rawVal * 0.1).toFixed(1);
canData.push({ time: msgDate.getTime(), speed: speed });
}
} }
// It sorts the LOCAL canData array.
canData.sort((a, b) => a.time - b.time);
}
// Sort the processed CAN data points by their timestamp.
canData.sort((a, b) => a.time - b.time);
console.log(`Processed ${canData.length} CAN messages for ID ${canIdToDecode}.`);
console.log(
`Processed ${canData.length} CAN messages for ID ${canIdToDecode}.`
);
// It returns the finished product in a structured object.
return { data: canData };
// It returns the finished product in a structured object.
// The processed CAN data is returned under the 'data' key.
return { data: canData };
} }
//--------------------JSON PARSER------------------------// //--------------------JSON PARSER------------------------//
// Add this new function to src/fileParsers.js
export function parseVisualizationJson(jsonString, radarStartTimeMs, videoStartDate) {
try {
const cleanJsonString = jsonString.replace(/\b(Infinity|NaN|-Infinity)\b/gi, 'null');
const vizData = JSON.parse(cleanJsonString);
if (!vizData.radarFrames || vizData.radarFrames.length === 0) {
return { error: 'Error: The JSON file does not contain any radar frames.' };
}
// Perform timestamp calculations
vizData.radarFrames.forEach(frame => {
frame.timestampMs = (radarStartTimeMs + frame.timestamp) - videoStartDate.getTime();
});
export function parseVisualizationJson(
jsonString,
radarStartTimeMs,
videoStartDate
) {
try {
// Replace Infinity, NaN, and -Infinity with "null" to prevent JSON.parse errors.
const cleanJsonString = jsonString.replace(
/\b(Infinity|NaN|-Infinity)\b/gi,
"null"
);
// Parse the cleaned JSON string into a JavaScript object.
const vizData = JSON.parse(cleanJsonString);
// Validate if the parsed data contains radar frames.
if (!vizData.radarFrames || vizData.radarFrames.length === 0) {
return {
error: "Error: The JSON file does not contain any radar frames.",
};
}
// Calculate SNR range from the data
let snrValues = [], totalPoints = 0;
vizData.radarFrames.forEach(frame => {
if (frame.pointCloud && frame.pointCloud.length > 0) {
totalPoints += frame.pointCloud.length;
frame.pointCloud.forEach(p => {
if (p.snr !== null) snrValues.push(p.snr);
});
}
// Perform timestamp calculations for each radar frame.
// The `timestampMs` for each frame is calculated relative to the video's start time,
// taking into account the `radarStartTimeMs` (extracted from JSON filename)
// and the `videoStartDate` (extracted from video filename).
// This ensures synchronization between radar data and video.
vizData.radarFrames.forEach((frame) => {
frame.timestampMs =
radarStartTimeMs + frame.timestamp - videoStartDate.getTime();
});
// Calculate SNR range from the data
let snrValues = [],
totalPoints = 0; // Counter for total points across all frames.
vizData.radarFrames.forEach((frame) => {
if (frame.pointCloud && frame.pointCloud.length > 0) {
totalPoints += frame.pointCloud.length;
frame.pointCloud.forEach((p) => {
// Collect SNR values, ignoring nulls.
if (p.snr !== null) snrValues.push(p.snr);
}); });
}
});
if (totalPoints === 0) {
console.warn('Warning: Loaded frames contain no point cloud data.');
}
const minSnr = snrValues.length > 0 ? Math.min(...snrValues) : 0;
const maxSnr = snrValues.length > 0 ? Math.max(...snrValues) : 1;
// Return the finished data package
return { data: vizData, minSnr: minSnr, maxSnr: maxSnr };
} catch (error) {
console.error("JSON Parsing Error:", error);
return { error: 'Error parsing JSON file. Please check file format. Error: ' + error.message };
// Warn if no point cloud data was found in the loaded frames.
if (totalPoints === 0) {
console.warn("Warning: Loaded frames contain no point cloud data.");
} }
// Determine the global minimum and maximum SNR values from the collected data.
// These values are used for scaling the SNR color legend.
// Default to 0 and 1 if no SNR values are found to prevent errors.
const minSnr = snrValues.length > 0 ? Math.min(...snrValues) : 0;
const maxSnr = snrValues.length > 0 ? Math.max(...snrValues) : 1;
// Return the finished data package
// This object contains the processed visualization data, and the calculated min/max SNR.
return { data: vizData, minSnr: minSnr, maxSnr: maxSnr };
} catch (error) {
console.error("JSON Parsing Error:", error);
return {
error:
"Error parsing JSON file. Please check file format. Error: " +
error.message,
};
}
} }

83
steps/src/main.js

@ -40,7 +40,7 @@ import {
findLastCanIndexBefore, findLastCanIndexBefore,
extractTimestampInfo, extractTimestampInfo,
parseTimestamp, parseTimestamp,
throttle
throttle,
} from "./utils.js"; } from "./utils.js";
// import state machine from './src/state.js'; // import state machine from './src/state.js';
import { appState } from "./state.js"; import { appState } from "./state.js";
@ -92,19 +92,21 @@ import {
updateCanDisplay, updateCanDisplay,
updateDebugOverlay, updateDebugOverlay,
} from "./dom.js"; } from "./dom.js";
// import modal dialog logic from './src/modal.js';
// Import modal dialog logic from './src/modal.js'.
import { showModal } from "./modal.js"; import { showModal } from "./modal.js";
// import initialize theme from './src/theme.js';
// Import theme initialization from './src/theme.js'.
import { initializeTheme } from "./theme.js"; import { initializeTheme } from "./theme.js";
// import caching logic from './src/db.js';
// Import caching logic from './src/db.js'.
import { initDB, saveFileToDB, loadFileFromDB } from "./db.js"; import { initDB, saveFileToDB, loadFileFromDB } from "./db.js";
// Sets up the video player with the given file URL.
function setupVideoPlayer(fileURL) { function setupVideoPlayer(fileURL) {
videoPlayer.src = fileURL; videoPlayer.src = fileURL;
videoPlayer.classList.remove("hidden"); videoPlayer.classList.remove("hidden");
videoPlaceholder.classList.add("hidden"); videoPlaceholder.classList.add("hidden");
videoPlayer.playbackRate = parseFloat(speedSlider.value); videoPlayer.playbackRate = parseFloat(speedSlider.value);
} }
// Event listener for loading JSON file.
loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); loadJsonBtn.addEventListener("click", () => jsonFileInput.click());
loadVideoBtn.addEventListener("click", () => videoFileInput.click()); loadVideoBtn.addEventListener("click", () => videoFileInput.click());
loadCanBtn.addEventListener("click", () => canFileInput.click()); loadCanBtn.addEventListener("click", () => canFileInput.click());
@ -116,6 +118,7 @@ clearCacheBtn.addEventListener("click", async () => {
window.location.reload(); window.location.reload();
} }
}); });
// Event listener for JSON file input change.
jsonFileInput.addEventListener("change", (event) => { jsonFileInput.addEventListener("change", (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
@ -174,6 +177,7 @@ jsonFileInput.addEventListener("change", (event) => {
}; };
reader.readAsText(file); reader.readAsText(file);
}); });
// Event listener for video file input change.
videoFileInput.addEventListener("change", (event) => { videoFileInput.addEventListener("change", (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
@ -222,7 +226,7 @@ videoFileInput.addEventListener("change", (event) => {
} }
}; };
}); });
// Event listener for CAN file input change.
canFileInput.addEventListener("change", (event) => { canFileInput.addEventListener("change", (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
@ -269,10 +273,12 @@ canFileInput.addEventListener("change", (event) => {
}; };
reader.readAsText(file); reader.readAsText(file);
}); });
// Event listener for offset input change.
offsetInput.addEventListener("input", () => { offsetInput.addEventListener("input", () => {
autoOffsetIndicator.classList.add("hidden"); autoOffsetIndicator.classList.add("hidden");
localStorage.setItem("visualizerOffset", offsetInput.value); localStorage.setItem("visualizerOffset", offsetInput.value);
}); });
// Event listener for apply SNR button click.
applySnrBtn.addEventListener("click", () => { applySnrBtn.addEventListener("click", () => {
const newMin = parseFloat(snrMinInput.value), const newMin = parseFloat(snrMinInput.value),
newMax = parseFloat(snrMaxInput.value); newMax = parseFloat(snrMaxInput.value);
@ -291,6 +297,7 @@ applySnrBtn.addEventListener("click", () => {
appState.p5_instance.redraw(); appState.p5_instance.redraw();
} }
}); });
// Event listener for play/pause button click.
playPauseBtn.addEventListener("click", () => { playPauseBtn.addEventListener("click", () => {
if (!appState.vizData && !videoPlayer.src) return; if (!appState.vizData && !videoPlayer.src) return;
appState.isPlaying = !appState.isPlaying; appState.isPlaying = !appState.isPlaying;
@ -307,6 +314,7 @@ playPauseBtn.addEventListener("click", () => {
if (videoPlayer.src) videoPlayer.pause(); if (videoPlayer.src) videoPlayer.pause();
} }
}); });
// Event listener for stop button click.
stopBtn.addEventListener("click", () => { stopBtn.addEventListener("click", () => {
videoPlayer.pause(); videoPlayer.pause();
appState.isPlaying = false; appState.isPlaying = false;
@ -318,18 +326,24 @@ stopBtn.addEventListener("click", () => {
} }
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
}); });
timelineSlider.addEventListener('input', throttle((event) => {
if (!appState.vizData) return;
if (appState.isPlaying) {
videoPlayer.pause();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
const frame = parseInt(event.target.value, 10);
updateFrame(frame, true);
appState.mediaTimeStart = videoPlayer.currentTime;
appState.masterClockStart = performance.now();
}, 16 )); // 50ms throttle delay
// Event listener for timeline slider input.
timelineSlider.addEventListener(
"input",
throttle((event) => {
if (!appState.vizData) return;
if (appState.isPlaying) {
videoPlayer.pause();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
const frame = parseInt(event.target.value, 10);
updateFrame(frame, true);
appState.mediaTimeStart = videoPlayer.currentTime;
appState.masterClockStart = performance.now();
}, 16)
); // Throttle delay for smoother updates.
// Currently set at 16 ms to achieve smooth 60fps.
// Event listener for speed slider input.
speedSlider.addEventListener("input", (event) => { speedSlider.addEventListener("input", (event) => {
const speed = parseFloat(event.target.value); const speed = parseFloat(event.target.value);
videoPlayer.playbackRate = speed; videoPlayer.playbackRate = speed;
@ -337,6 +351,7 @@ speedSlider.addEventListener("input", (event) => {
}); });
// ADD THE NEW TOGGLE TO THE ARRAY // ADD THE NEW TOGGLE TO THE ARRAY
// Array of color toggles.
const colorToggles = [ const colorToggles = [
toggleSnrColor, toggleSnrColor,
toggleClusterColor, toggleClusterColor,
@ -353,14 +368,14 @@ colorToggles.forEach((t) => {
if (appState.p5_instance) appState.p5_instance.redraw(); if (appState.p5_instance) appState.p5_instance.redraw();
}); });
}); });
// Event listeners for various feature toggles.
[ [
toggleVelocity, toggleVelocity,
toggleEgoSpeed, toggleEgoSpeed,
toggleFrameNorm, toggleFrameNorm,
toggleTracks, toggleTracks,
toggleDebugOverlay, toggleDebugOverlay,
toggleDebug2Overlay
toggleDebug2Overlay,
].forEach((t) => { ].forEach((t) => {
t.addEventListener("change", () => { t.addEventListener("change", () => {
if (appState.p5_instance) { if (appState.p5_instance) {
@ -371,10 +386,12 @@ colorToggles.forEach((t) => {
); );
appState.p5_instance.redraw(); appState.p5_instance.redraw();
} }
if (t === toggleDebugOverlay || t === toggleDebug2Overlay) { updateDebugOverlay(videoPlayer.currentTime)};
if (t === toggleDebugOverlay || t === toggleDebug2Overlay) {
updateDebugOverlay(videoPlayer.currentTime);
}
}); });
}); });
// Event listener for close-up toggle.
toggleCloseUp.addEventListener("change", () => { toggleCloseUp.addEventListener("change", () => {
appState.isCloseUpMode = toggleCloseUp.checked; appState.isCloseUpMode = toggleCloseUp.checked;
if (appState.p5_instance) { if (appState.p5_instance) {
@ -389,11 +406,12 @@ toggleCloseUp.addEventListener("change", () => {
} }
} }
}); });
// Event listener for video ended event.
videoPlayer.addEventListener("ended", () => { videoPlayer.addEventListener("ended", () => {
appState.isPlaying = false; appState.isPlaying = false;
playPauseBtn.textContent = "Play"; playPauseBtn.textContent = "Play";
}); });
// Event listener for keyboard arrow key presses to navigate frames.
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if ( if (
!appState.vizData || !appState.vizData ||
@ -420,6 +438,7 @@ document.addEventListener("keydown", (event) => {
appState.masterClockStart = performance.now(); appState.masterClockStart = performance.now();
} }
}); });
// Calculates and sets the time offset between JSON and video timestamps.
function calculateAndSetOffset() { function calculateAndSetOffset() {
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
@ -453,10 +472,10 @@ function calculateAndSetOffset() {
} }
} }
// --- Application Initialization ---
// Application Initialization: Event listener for DOMContentLoaded.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initializeTheme(); initializeTheme();
console.log("DEBUG: DOMContentLoaded fired. Starting session load.");
console.log("DEBUG: DOMContentLoaded fired. Starting session load."); // Log for debugging.
initDB(() => { initDB(() => {
console.log("DEBUG: Database initialized."); console.log("DEBUG: Database initialized.");
@ -469,8 +488,9 @@ document.addEventListener("DOMContentLoaded", () => {
appState.canLogFilename = localStorage.getItem("canLogFilename"); appState.canLogFilename = localStorage.getItem("canLogFilename");
// This is important: it sets videoStartDate if a video filename is cached // This is important: it sets videoStartDate if a video filename is cached
calculateAndSetOffset();
calculateAndSetOffset(); // Calculate offset based on cached filenames.
// Promises to load files from IndexedDB.
const videoPromise = new Promise((resolve) => const videoPromise = new Promise((resolve) =>
loadFileFromDB("video", resolve) loadFileFromDB("video", resolve)
); );
@ -480,7 +500,7 @@ document.addEventListener("DOMContentLoaded", () => {
const canLogPromise = new Promise((resolve) => const canLogPromise = new Promise((resolve) =>
loadFileFromDB("canLogText", resolve) loadFileFromDB("canLogText", resolve)
); );
// Once all files are loaded from DB, process them.
Promise.all([videoPromise, jsonPromise, canLogPromise]) Promise.all([videoPromise, jsonPromise, canLogPromise])
.then(([videoBlob, jsonString, canLogText]) => { .then(([videoBlob, jsonString, canLogText]) => {
console.log("DEBUG: All data fetched from IndexedDB."); console.log("DEBUG: All data fetched from IndexedDB.");
@ -488,7 +508,7 @@ document.addEventListener("DOMContentLoaded", () => {
const processAllData = () => { const processAllData = () => {
console.log("DEBUG: Processing all loaded data."); console.log("DEBUG: Processing all loaded data.");
// 1. Process JSON (only if we have a video date)
// 1. Process JSON (only if video start date is available).
if (jsonString && appState.videoStartDate) { if (jsonString && appState.videoStartDate) {
const result = parseVisualizationJson( const result = parseVisualizationJson(
jsonString, jsonString,
@ -506,7 +526,7 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// 2. Process CAN log (only if we have a video date)
// 2. Process CAN log (only if video start date is available).
if (canLogText && appState.videoStartDate) { if (canLogText && appState.videoStartDate) {
const result = processCanLog(canLogText, appState.videoStartDate); const result = processCanLog(canLogText, appState.videoStartDate);
if (!result.error) { if (!result.error) {
@ -514,7 +534,7 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// 3. Update all UI elements now that data is processed
// 3. Update all UI elements now that data is processed.
if (appState.vizData) { if (appState.vizData) {
resetVisualization(); resetVisualization();
canvasPlaceholder.style.display = "none"; canvasPlaceholder.style.display = "none";
@ -536,15 +556,14 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}; };
// This is the main controller
// --- THIS IS THE CORRECTED CODE ---
// Main controller for processing data based on video availability.
if (videoBlob) { if (videoBlob) {
const fileURL = URL.createObjectURL(videoBlob); const fileURL = URL.createObjectURL(videoBlob);
setupVideoPlayer(fileURL); setupVideoPlayer(fileURL);
// This ensures we ONLY process data once the video's duration is known. // This ensures we ONLY process data once the video's duration is known.
videoPlayer.onloadedmetadata = processAllData; videoPlayer.onloadedmetadata = processAllData;
} else { } else {
// If there's no video, we can go ahead and process the other data.
// If there's no video, process other data immediately.
processAllData(); processAllData();
} }
}) })

73
steps/src/modal.js

@ -1,33 +1,46 @@
import { modalText, modalCancelBtn, modalContainer, modalOverlay, modalContent, modalOkBtn } from './dom.js';
import {
modalText,
modalCancelBtn,
modalContainer,
modalOverlay,
modalContent,
modalOkBtn,
} from "./dom.js";
// --- Custom Modal Logic --- // // --- Custom Modal Logic --- //
let modalResolve = null;
export function showModal(message, isConfirm = false) {
return new Promise(resolve => {
modalText.textContent = message;
modalCancelBtn.classList.toggle('hidden', !isConfirm);
modalContainer.classList.remove('hidden');
setTimeout(() => {
modalOverlay.classList.remove('opacity-0');
modalContent.classList.remove('scale-95');
}
, 10);
modalResolve = resolve;
});
}
function hideModal(value) {
modalOverlay.classList.add('opacity-0');
modalContent.classList.add('scale-95');
setTimeout(() => {
modalContainer.classList.add('hidden');
if (modalResolve) modalResolve(value);
}, 200);
}
//----------------------Modal Event Listeners----------------------//
// Variable to store the resolve function of the Promise, allowing the modal to return a value.
let modalResolve = null;
export function showModal(message, isConfirm = false) {
return new Promise((resolve) => {
// Set the message text for the modal.
modalText.textContent = message;
// Show/hide the cancel button based on whether it's a confirmation modal.
modalCancelBtn.classList.toggle("hidden", !isConfirm);
// Make the modal container visible.
modalContainer.classList.remove("hidden");
// Add a slight delay for CSS transitions to take effect, making the modal appear smoothly.
setTimeout(() => {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
// Store the resolve function to be called when the modal is closed.
modalResolve = resolve;
});
}
// Hides the modal and resolves the Promise with the given value.
function hideModal(value) {
modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95");
setTimeout(() => {
modalContainer.classList.add("hidden");
if (modalResolve) modalResolve(value);
}, 200);
}
modalOkBtn.addEventListener('click', () => hideModal(true));
modalCancelBtn.addEventListener('click', () => hideModal(false));
modalOverlay.addEventListener('click', () => hideModal(false));
//----------------------Modal Event Listeners----------------------//
// Event listener for the "OK" button. Resolves the modal Promise with 'true'.
modalOkBtn.addEventListener("click", () => hideModal(true));
// Event listener for the "Cancel" button. Resolves the modal Promise with 'false'.
modalCancelBtn.addEventListener("click", () => hideModal(false));
// Event listener for clicking on the modal overlay (outside the content). Resolves the modal Promise with 'false'.
modalOverlay.addEventListener("click", () => hideModal(false));

264
steps/src/p5/radarSketch.js

@ -1,126 +1,170 @@
import { appState } from "../state.js";
import { import {
appState
} from '../state.js';
RADAR_X_MAX,
// Define radar plot boundaries
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
} from "../constants.js";
import { canvasContainer, toggleSnrColor, toggleTracks } from "../dom.js";
import { import {
RADAR_X_MAX,
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN
} from '../constants.js';
import {
canvasContainer,
toggleSnrColor,
toggleTracks
} from '../dom.js';
import {
drawStaticRegionsToBuffer,
drawAxes,
drawPointCloud,
drawTrajectories,
drawTrackMarkers,
snrColors,
handleCloseUpDisplay // BUG FIX 1: Import the close-up handler
} from '../drawUtils.js';
drawStaticRegionsToBuffer,
drawAxes,
drawPointCloud,
// Import drawing utility functions
drawTrajectories,
drawTrackMarkers,
snrColors,
handleCloseUpDisplay, // BUG FIX 1: Import the close-up handler
} from "../drawUtils.js";
export const radarSketch = function(p) {
let plotScales = {
plotScaleX: 1,
plotScaleY: 1
};
let staticBackgroundBuffer, snrLegendBuffer;
function calculatePlotScales() {
const hPad = 0.05,
vPad = 0.05,
bOff = 0.05;
const aW = p.width * (1 - 2 * hPad);
const aH = p.height * (1 - bOff - vPad);
plotScales.plotScaleX = aW / (RADAR_X_MAX - RADAR_X_MIN);
plotScales.plotScaleY = aH / (RADAR_Y_MAX - RADAR_Y_MIN);
}
export const radarSketch = function (p) {
// Object to store calculated plot scales
let plotScales = {
plotScaleX: 1,
plotScaleY: 1,
};
// p5.Graphics buffers for static elements to optimize drawing
let staticBackgroundBuffer, snrLegendBuffer;
p.setup = function() {
let canvas = p.createCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
canvas.parent('canvas-container');
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
snrLegendBuffer = p.createGraphics(100, 450);
// Function to calculate scaling factors for radar coordinates to canvas pixels
function calculatePlotScales() {
// Padding and offset values for the plot area
const hPad = 0.05,
vPad = 0.05,
bOff = 0.05;
// 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);
}
calculatePlotScales();
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
p.noLoop();
};
p.setup = function () {
// 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");
// Initialize graphics buffers
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
snrLegendBuffer = p.createGraphics(100, 450);
p.draw = function() {
p.background(document.documentElement.classList.contains('dark') ? p.color(55, 65, 81) : 255);
if (!appState.vizData) return;
calculatePlotScales();
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
p.noLoop();
// Disable continuous looping, redraw will be called manually
};
p.image(staticBackgroundBuffer, 0, 0);
p.draw = function () {
// Set background color based on current theme (dark/light)
p.background(
document.documentElement.classList.contains("dark")
? p.color(55, 65, 81)
: 255
);
// If no visualization data is loaded, stop drawing
if (!appState.vizData) return;
p.push();
p.translate(p.width / 2, p.height * 0.95);
p.scale(1, -1);
// Draw the pre-rendered static background elements
p.image(staticBackgroundBuffer, 0, 0);
calculatePlotScales();
drawAxes(p, plotScales);
// Apply transformations for radar coordinate system (origin at bottom-center, Y-axis inverted)
p.push();
p.translate(p.width / 2, p.height * 0.95);
p.scale(1, -1);
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (frameData) {
if (toggleTracks.checked) {
drawTrajectories(p, plotScales);
drawTrackMarkers(p, plotScales);
}
drawPointCloud(p, frameData.pointCloud, plotScales);
}
p.pop();
// Recalculate plot scales (important for window resizing)
calculatePlotScales();
// Draw coordinate axes
drawAxes(p, plotScales);
// BUG FIX 1: Call the close-up handler if the mode is active
if (appState.isCloseUpMode) {
handleCloseUpDisplay(p, plotScales);
}
// Get current frame data
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (frameData) {
// Draw object trajectories and markers if enabled
if (toggleTracks.checked) {
drawTrajectories(p, plotScales);
drawTrackMarkers(p, plotScales);
}
// Draw the point cloud for the current frame
drawPointCloud(p, frameData.pointCloud, plotScales);
}
p.pop();
if (toggleSnrColor.checked) {
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10);
}
};
// BUG FIX 1: Call the close-up handler if the mode is active
if (appState.isCloseUpMode) {
handleCloseUpDisplay(p, plotScales);
}
p.drawSnrLegendToBuffer = function(minV, maxV) {
const b = snrLegendBuffer;
const localSnrColors = snrColors(p);
b.clear();
b.push();
const lx = 10,
ly = 20,
lw = 15,
lh = 400;
for (let i = 0; i < lh; i++) {
const amt = b.map(i, 0, lh, 1, 0);
let c;
if (amt < 0.25) c = b.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
else if (amt < 0.5) c = b.lerpColor(localSnrColors.c2, localSnrColors.c3, (amt - 0.25) / 0.25);
else if (amt < 0.75) c = b.lerpColor(localSnrColors.c3, localSnrColors.c4, (amt - 0.5) / 0.25);
else c = b.lerpColor(localSnrColors.c4, localSnrColors.c5, (amt - 0.75) / 0.25);
b.stroke(c);
b.line(lx, ly + i, lx + lw, ly + i);
}
b.fill(document.documentElement.classList.contains('dark') ? 255 : 0);
b.noStroke();
b.textSize(10);
b.textAlign(b.LEFT, b.CENTER);
b.text(maxV.toFixed(1), lx + lw + 5, ly);
b.text(minV.toFixed(1), lx + lw + 5, ly + lh);
b.text("SNR", lx, ly - 10);
b.pop();
};
// Draw the SNR legend if enabled
if (toggleSnrColor.checked) {
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10);
}
};
// Function to draw the SNR legend to its buffer
p.drawSnrLegendToBuffer = function (minV, maxV) {
// Reference to the SNR legend buffer
const b = snrLegendBuffer;
const localSnrColors = snrColors(p);
b.clear();
b.push();
const lx = 10,
ly = 20,
lw = 15,
// Dimensions for the color bar
lh = 400;
for (let i = 0; i < lh; i++) {
const amt = b.map(i, 0, lh, 1, 0);
let c;
if (amt < 0.25)
c = b.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
else if (amt < 0.5)
c = b.lerpColor(
localSnrColors.c2,
localSnrColors.c3,
(amt - 0.25) / 0.25
);
else if (amt < 0.75)
c = b.lerpColor(
localSnrColors.c3,
localSnrColors.c4,
(amt - 0.5) / 0.25
);
else
c = b.lerpColor(
localSnrColors.c4,
localSnrColors.c5,
// Interpolate colors based on position
(amt - 0.75) / 0.25
);
b.stroke(c);
b.line(lx, ly + i, lx + lw, ly + i);
}
// Set text color based on theme
b.fill(document.documentElement.classList.contains("dark") ? 255 : 0);
b.noStroke();
b.textSize(10);
b.textAlign(b.LEFT, b.CENTER);
// Draw min/max SNR values and label
b.text(maxV.toFixed(1), lx + lw + 5, ly);
b.text(minV.toFixed(1), lx + lw + 5, ly + lh);
b.text("SNR", lx, ly - 10);
b.pop();
};
p.windowResized = function() {
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
// BUG FIX 2: Re-create the buffer instead of resizing it
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
calculatePlotScales();
// Re-draw the static content to the new buffer
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
if (appState.vizData) p.redraw();
};
// Handle window resizing event
p.windowResized = function () {
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
// BUG FIX 2: Re-create the buffer instead of resizing it
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
calculatePlotScales();
// Re-draw the static content to the new buffer
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
if (appState.vizData) p.redraw();
};
}; };

407
steps/src/p5/speedGraphSketch.js

@ -1,170 +1,265 @@
//---Import APPSTATE VIDEOPLAYER and FindLastCanIndex---// //---Import APPSTATE VIDEOPLAYER and FindLastCanIndex---//
import { appState
} from '../state.js';
import { videoPlayer, speedGraphContainer
} from '../dom.js';
import { findLastCanIndexBefore
} from '../utils.js';
import { appState } from "../state.js";
import { videoPlayer, speedGraphContainer } from "../dom.js";
import { findLastCanIndexBefore } from "../utils.js";
export const speedGraphSketch = function (p) { export const speedGraphSketch = function (p) {
let staticBuffer, minSpeed, maxSpeed, videoDuration;
const pad = { top: 20, right: 130, bottom: 30, left: 50 };
// This function is now attached to the p5 instance, making it public
// It's responsible for drawing the static background and data lines
p.drawStaticGraphToBuffer = function (canSpeedData, radarData) {
const b = staticBuffer;
b.clear();
const isDark = document.documentElement.classList.contains('dark');
b.background(isDark ? [55, 65, 81] : 255);
const gridColor = isDark ? 100 : 200;
const textColor = isDark ? 200 : 100;
b.push();
b.stroke(gridColor);
b.strokeWeight(1);
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom);
b.line(pad.left, b.height - pad.bottom, b.width - pad.right, b.height - pad.bottom);
b.textAlign(b.RIGHT, b.CENTER);
b.noStroke();
b.fill(textColor);
b.textSize(10);
for (let s = minSpeed; s <= maxSpeed; s += 10) {
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.text(s, pad.left - 8, y);
if (s === 0) {
b.strokeWeight(1.5);
b.stroke(isDark ? 150 : 180);
} else {
b.strokeWeight(1);
b.stroke(isDark ? 80 : 230);
}
b.line(pad.left + 1, y, b.width - pad.right, y);
b.noStroke();
}
b.fill(textColor);
b.text("km/h", pad.left - 8, pad.top - 8);
b.textAlign(b.CENTER, b.TOP);
b.noStroke();
b.fill(isDark ? 180 : 150);
const tInt = Math.max(1, Math.floor(videoDuration / 10));
for (let t = 0; t <= videoDuration; t += tInt) { const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right); b.text(Math.round(t), x, b.height - pad.bottom + 5); }
b.fill(textColor);
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18);
b.pop();
if (canSpeedData && canSpeedData.length > 0) {
b.noFill();
b.stroke(0, 150, 255);
b.strokeWeight(1.5);
b.beginShape();
for (const d of canSpeedData) { const relTime = (d.time - appState.videoStartDate.getTime()) / 1000; if (relTime >= 0 && relTime <= videoDuration) { const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); const y = b.map(d.speed, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); b.vertex(x, y); } }
b.endShape();
}
// Declare variables for the static buffer, min/max speed for scaling, and video duration.
let staticBuffer, minSpeed, maxSpeed, videoDuration;
// Define padding for the graph to ensure elements are not drawn at the edges.
const pad = { top: 20, right: 130, bottom: 30, left: 50 };
if (radarData && radarData.radarFrames) {
b.stroke(0, 200, 100);
b.drawingContext.setLineDash([5, 5]);
b.beginShape();
for (const frame of radarData.radarFrames) {
const relTime = frame.timestampMs / 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;
const y = b.map(egoSpeedKmh, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.vertex(x, y);
}
}
b.endShape();
b.drawingContext.setLineDash([]);
}
/**
* Draws the static elements of the speed graph (axes, grid, labels, and data lines)
* to an off-screen buffer. This optimizes performance by not redrawing these elements
* every frame.
* @param {Array} canSpeedData - Array of CAN speed data points.
* @param {Object} radarData - Object containing radar frames with ego velocity.
*/
// This function is now attached to the p5 instance, making it public
// It's responsible for drawing the static background and data lines
p.drawStaticGraphToBuffer = function (canSpeedData, radarData) {
const b = staticBuffer;
b.clear();
const isDark = document.documentElement.classList.contains("dark");
b.background(isDark ? [55, 65, 81] : 255);
const gridColor = isDark ? 100 : 200;
const textColor = isDark ? 200 : 100; // Determine text color based on theme.
b.push();
b.strokeWeight(2);
b.noStroke();
b.fill(textColor);
b.textAlign(b.LEFT, b.CENTER);
b.stroke(0, 150, 255);
b.line(b.width - 120, pad.top + 10, b.width - 100, pad.top + 10);
b.noStroke();
b.text("CAN Speed", b.width - 95, pad.top + 10);
b.stroke(0, 200, 100);
b.drawingContext.setLineDash([3, 3]);
b.line(b.width - 120, pad.top + 30, b.width - 100, pad.top + 30);
b.drawingContext.setLineDash([]);
b.noStroke();
b.text("Ego Speed", b.width - 95, pad.top + 30);
b.pop();
};
// Push current drawing style settings onto a stack.
b.push();
// Set stroke for grid lines.
b.stroke(gridColor);
// Set stroke weight for grid lines.
b.strokeWeight(1);
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom);
b.line(
pad.left,
b.height - pad.bottom,
b.width - pad.right,
b.height - pad.bottom
); // Draw Y and X axes.
// Set text alignment for Y-axis labels.
b.textAlign(b.RIGHT, b.CENTER);
b.noStroke();
b.fill(textColor);
// Set text size for labels.
b.textSize(10);
// Draw horizontal grid lines and speed labels.
for (let s = minSpeed; s <= maxSpeed; s += 10) {
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.text(s, pad.left - 8, y);
if (s === 0) {
b.strokeWeight(1.5);
b.stroke(isDark ? 150 : 180);
} else {
b.strokeWeight(1);
b.stroke(isDark ? 80 : 230);
}
b.line(pad.left + 1, y, b.width - pad.right, y);
b.noStroke();
}
// Draw Y-axis unit label.
b.fill(textColor);
b.text("km/h", pad.left - 8, pad.top - 8);
// Set text alignment for X-axis labels.
b.textAlign(b.CENTER, b.TOP);
b.noStroke();
b.fill(isDark ? 180 : 150);
// Calculate time interval for X-axis labels.
const tInt = Math.max(1, Math.floor(videoDuration / 10));
for (let t = 0; t <= videoDuration; t += tInt) {
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right);
b.text(Math.round(t), x, b.height - pad.bottom + 5);
}
b.fill(textColor);
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18);
// Restore previous drawing style settings.
b.pop();
p.setup = function () {
let canvas = p.createCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight);
canvas.parent('speed-graph-container');
staticBuffer = p.createGraphics(p.width, p.height);
p.noLoop();
};
// Draw CAN speed data line if available.
if (canSpeedData && canSpeedData.length > 0) {
b.noFill(); // Do not fill the shape.
b.stroke(0, 150, 255);
b.strokeWeight(1.5);
b.beginShape();
for (const d of canSpeedData) {
const relTime = (d.time - appState.videoStartDate.getTime()) / 1000;
if (relTime >= 0 && relTime <= videoDuration) {
const x = b.map(
relTime,
0,
videoDuration,
pad.left,
b.width - pad.right
);
const y = b.map(
d.speed,
minSpeed,
maxSpeed,
b.height - pad.bottom,
pad.top
);
b.vertex(x, y);
}
}
b.endShape();
} // End of CAN speed data drawing.
p.setData = function (canSpeedData, radarData, duration) {
if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return;
videoDuration = duration;
// Draw radar ego speed data line if available.
if (radarData && radarData.radarFrames) {
b.stroke(0, 200, 100);
b.drawingContext.setLineDash([5, 5]);
b.beginShape();
for (const frame of radarData.radarFrames) {
const relTime = frame.timestampMs / 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;
const y = b.map(
egoSpeedKmh,
minSpeed,
maxSpeed,
b.height - pad.bottom,
pad.top
);
b.vertex(x, y);
}
}
b.endShape();
b.drawingContext.setLineDash([]); // Reset line dash to solid.
} // End of radar ego speed data drawing.
let speeds = [];
if (canSpeedData) {
speeds.push(...canSpeedData.map(d => parseFloat(d.speed)));
}
if (radarData && radarData.radarFrames) {
const egoSpeeds = radarData.radarFrames.map(frame => frame.egoVelocity[1] * 3.6);
speeds.push(...egoSpeeds);
}
// Draw legend for the graph lines.
b.push();
b.strokeWeight(2);
b.noStroke();
b.fill(textColor);
b.textAlign(b.LEFT, b.CENTER);
b.stroke(0, 150, 255);
b.line(b.width - 120, pad.top + 10, b.width - 100, pad.top + 10);
b.noStroke();
b.text("CAN Speed", b.width - 95, pad.top + 10);
b.stroke(0, 200, 100);
b.drawingContext.setLineDash([3, 3]);
b.line(b.width - 120, pad.top + 30, b.width - 100, pad.top + 30);
b.drawingContext.setLineDash([]);
b.noStroke();
b.text("Ego Speed", b.width - 95, pad.top + 30);
b.pop();
};
/**
* p5.js setup function. Initializes the canvas and static buffer.
*/
p.setup = function () {
let canvas = p.createCanvas(
speedGraphContainer.offsetWidth,
speedGraphContainer.offsetHeight
);
canvas.parent("speed-graph-container");
// Create an off-screen graphics buffer for static elements.
staticBuffer = p.createGraphics(p.width, p.height);
// Disable continuous looping; draw will be called manually.
p.noLoop();
};
/**
* Sets the data for the speed graph and recalculates min/max speed for scaling.
* @param {Array} canSpeedData - Array of CAN speed data points.
* @param {Object} radarData - Object containing radar frames with ego velocity.
* @param {number} duration - The total duration of the video in seconds.
*/
p.setData = function (canSpeedData, radarData, duration) {
if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return; // Exit if no data.
videoDuration = duration;
minSpeed = speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0;
maxSpeed = speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 10;
if (maxSpeed <= 0) maxSpeed = 10;
if (minSpeed >= 0) minSpeed = 0;
let speeds = [];
if (canSpeedData) {
speeds.push(...canSpeedData.map((d) => parseFloat(d.speed)));
}
if (radarData && radarData.radarFrames) {
const egoSpeeds = radarData.radarFrames.map(
(frame) => frame.egoVelocity[1] * 3.6
);
speeds.push(...egoSpeeds);
}
p.drawStaticGraphToBuffer(canSpeedData, radarData);
p.redraw();
};
// Calculate min and max speeds for Y-axis scaling, rounding to nearest 10.
minSpeed =
speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0;
maxSpeed =
speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 10;
// Ensure maxSpeed is at least 10 if all speeds are non-positive.
if (maxSpeed <= 0) maxSpeed = 10;
// Ensure minSpeed is 0 if all speeds are non-negative.
if (minSpeed >= 0) minSpeed = 0;
p.draw = function () {
if (!videoDuration) return;
p.image(staticBuffer, 0, 0);
drawTimeIndicator();
};
// Redraw the static graph elements to the buffer with new data.
p.drawStaticGraphToBuffer(canSpeedData, radarData);
// Request a redraw of the main canvas.
p.redraw();
};
/**
* p5.js draw function. Draws the static buffer and the dynamic time indicator.
*/
p.draw = function () {
if (!videoDuration) return; // Only draw if video duration is set.
p.image(staticBuffer, 0, 0);
drawTimeIndicator();
};
function drawTimeIndicator() {
const currentTime = videoPlayer.currentTime;
const x = p.map(currentTime, 0, videoDuration, pad.left, p.width - pad.right);
p.stroke(255, 0, 0, 150);
p.strokeWeight(1.5);
p.line(x, pad.top, x, p.height - pad.bottom);
const videoAbsTimeMs = appState.videoStartDate.getTime() + (currentTime * 1000);
const canIndex = findLastCanIndexBefore(videoAbsTimeMs, appState.canData);
if (canIndex !== -1) {
const canMsg = appState.canData[canIndex];
const y = p.map(canMsg.speed, minSpeed, maxSpeed, p.height - pad.bottom, pad.top);
p.fill(255, 0, 0);
p.noStroke();
p.ellipse(x, y, 8, 8);
}
}
function drawTimeIndicator() {
const currentTime = videoPlayer.currentTime;
const x = p.map(
currentTime,
0,
videoDuration,
pad.left,
p.width - pad.right
); // Map current time to X-coordinate.
// Draw the red time indicator line.
p.stroke(255, 0, 0, 150);
p.strokeWeight(1.5);
p.line(x, pad.top, x, p.height - pad.bottom);
p.windowResized = function () {
p.resizeCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight);
// Instead of resizing the buffer, we re-create it
staticBuffer = p.createGraphics(p.width, p.height);
// And we must re-draw the static content to the new buffer
if ((appState.canData.length > 0 || appState.vizData) && videoDuration) {
p.drawStaticGraphToBuffer(appState.canData, appState.vizData);
}
p.redraw();
};
};
// Draw a circle on the CAN speed line at the current time.
const videoAbsTimeMs =
appState.videoStartDate.getTime() + currentTime * 1000;
const canIndex = findLastCanIndexBefore(videoAbsTimeMs, appState.canData);
if (canIndex !== -1) {
const canMsg = appState.canData[canIndex];
const y = p.map(
canMsg.speed,
minSpeed,
maxSpeed,
p.height - pad.bottom,
pad.top
);
p.fill(255, 0, 0);
p.noStroke(); // No stroke for the ellipse.
p.ellipse(x, y, 8, 8);
}
}
/**
* Handles window resizing. Resizes the canvas and recreates/redraws the static buffer.
*/
p.windowResized = function () {
p.resizeCanvas(
speedGraphContainer.offsetWidth,
speedGraphContainer.offsetHeight
);
// Instead of resizing the buffer, we re-create it
staticBuffer = p.createGraphics(p.width, p.height);
// And we must re-draw the static content to the new buffer
if ((appState.canData.length > 0 || appState.vizData) && videoDuration) {
p.drawStaticGraphToBuffer(appState.canData, appState.vizData);
}
p.redraw();
};
};

53
steps/src/state.js

@ -1,19 +1,38 @@
export const appState = { export const appState = {
vizData : null,
canData : [],
rawCanLogText : null,
videoStartDate : null,
radarStartTimeMs : 0,
isPlaying : false,
currentFrame : 0,
globalMinSnr : 0, globalMaxSnr : 1,
p5_instance : null, speedGraphInstance : null,
jsonFilename : '', videoFilename : '', canLogFilename : '',
isCloseUpMode : false,
masterClockStart : 0,
mediaTimeStart : 0,
lastSyncTime : 0,
// Stores the parsed visualization data (radar frames, tracks, etc.)
vizData: null,
// Stores the processed CAN bus data (speed, time)
canData: [],
// Temporarily holds raw CAN log text if video start date is not yet available for processing
rawCanLogText: null,
// The Date object representing the start time of the video
videoStartDate: null,
// The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename
radarStartTimeMs: 0,
// Boolean indicating if the playback is currently active
isPlaying: false,
// The index of the currently displayed radar frame
currentFrame: 0,
// The global minimum SNR value across all radar frames, used for color scaling
globalMinSnr: 0,
// The global maximum SNR value across all radar frames, used for color scaling
globalMaxSnr: 1,
// Reference to the p5.js instance for the radar visualization
p5_instance: null,
// Reference to the p5.js instance for the speed graph visualization
speedGraphInstance: null,
// The filename of the loaded JSON file
jsonFilename: "",
// The filename of the loaded video file
videoFilename: "",
// The filename of the loaded CAN log file
canLogFilename: "",
// Boolean indicating if the close-up interaction mode is active
isCloseUpMode: false,
// Timestamp (from performance.now()) when the master clock started for synchronized playback
masterClockStart: 0,
// The media time (in seconds) of the video when the master clock started
mediaTimeStart: 0,
// Timestamp (from performance.now()) of the last synchronization check
lastSyncTime: 0,
}; };

96
steps/src/sync.js

@ -1,6 +1,14 @@
import { appState } from './state.js';
import { videoPlayer, speedSlider, offsetInput, stopBtn, updateFrame, updateCanDisplay, updateDebugOverlay } from './dom.js';
import { findRadarFrameIndexForTime } from './utils.js';
import { appState } from "./state.js";
import {
videoPlayer,
speedSlider,
offsetInput,
stopBtn,
updateFrame,
updateCanDisplay,
updateDebugOverlay,
} from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js";
/** /**
* The main animation loop that drives the synchronized playback. * The main animation loop that drives the synchronized playback.
@ -8,45 +16,61 @@ import { findRadarFrameIndexForTime } from './utils.js';
* finds the corresponding radar frame, and handles resynchronization with the video element. * finds the corresponding radar frame, and handles resynchronization with the video element.
*/ */
export function animationLoop() { export function animationLoop() {
if (!appState.isPlaying) return;
if (!appState.isPlaying) return;
const playbackSpeed = parseFloat(speedSlider.value);
const elapsedRealTime = performance.now() - appState.masterClockStart;
const currentMediaTime = appState.mediaTimeStart + (elapsedRealTime / 1000) * playbackSpeed;
// 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
if (appState.vizData && appState.videoStartDate) {
const offsetMs = parseFloat(offsetInput.value) || 0;
const targetRadarTimeMs = (currentMediaTime * 1000);
const targetFrame = findRadarFrameIndexForTime(targetRadarTimeMs, appState.vizData);
if (targetFrame !== appState.currentFrame) {
updateFrame(targetFrame, false);
}
// 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
const offsetMs = parseFloat(offsetInput.value) || 0;
// Calculate the target radar time in milliseconds
const targetRadarTimeMs = currentMediaTime * 1000;
// Find the index of the radar frame that corresponds to the target time
const targetFrame = findRadarFrameIndexForTime(
targetRadarTimeMs,
appState.vizData
);
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 > 500) {
const videoTime = videoPlayer.currentTime;
const drift = Math.abs(currentMediaTime - videoTime);
if (drift > 0.15) { // Resync if drift is > 150ms
console.warn(`Resyncing video. Drift was: ${drift.toFixed(3)}s`);
videoPlayer.currentTime = currentMediaTime;
}
appState.lastSyncTime = now;
// Periodically check for drift between master clock and video element
const now = performance.now();
if (now - appState.lastSyncTime > 500) {
const videoTime = videoPlayer.currentTime;
const drift = Math.abs(currentMediaTime - videoTime);
// Resync if drift is > 150ms
if (drift > 0.15) {
// Resync if drift is > 150ms
console.warn(`Resyncing video. Drift was: ${drift.toFixed(3)}s`);
videoPlayer.currentTime = currentMediaTime;
} }
appState.lastSyncTime = now;
}
// Stop playback at the end of the video
if (currentMediaTime >= videoPlayer.duration) {
stopBtn.click();
return;
}
// Stop playback at the end of the video
if (currentMediaTime >= videoPlayer.duration) {
stopBtn.click();
return;
}
// Update other UI elements
updateCanDisplay(currentMediaTime);
updateDebugOverlay(currentMediaTime);
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// Update CAN bus data display
updateCanDisplay(currentMediaTime);
// Update debug overlay information
updateDebugOverlay(currentMediaTime);
// Redraw the speed graph if an instance exists
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// Request the next frame
requestAnimationFrame(animationLoop);
// Request the next frame
requestAnimationFrame(animationLoop);
} }

90
steps/src/theme.js

@ -1,54 +1,58 @@
import { appState } from './state.js';
import { videoPlayer } from './dom.js';
// --- DARK MODE: Step 3 - Add the JavaScript Logic ---
const themeToggleBtn = document.getElementById('theme-toggle');
const darkIcon = document.getElementById('theme-toggle-dark-icon');
const lightIcon = document.getElementById('theme-toggle-light-icon');
import { appState } from "./state.js";
import { videoPlayer } from "./dom.js";
const themeToggleBtn = document.getElementById("theme-toggle");
const darkIcon = document.getElementById("theme-toggle-dark-icon");
const lightIcon = document.getElementById("theme-toggle-light-icon");
function setTheme(theme) { function setTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
lightIcon.classList.remove('hidden');
darkIcon.classList.add('hidden');
localStorage.setItem('color-theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
darkIcon.classList.remove('hidden');
lightIcon.classList.add('hidden');
localStorage.setItem('color-theme', 'light');
}
if (theme === "dark") {
document.documentElement.classList.add("dark");
lightIcon.classList.remove("hidden");
darkIcon.classList.add("hidden");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
darkIcon.classList.remove("hidden");
lightIcon.classList.add("hidden");
localStorage.setItem("color-theme", "light");
}
// Redraw the main radar plot
if (appState.p5_instance) appState.p5_instance.redraw();
// Redraw the main radar plot to apply theme changes
if (appState.p5_instance) appState.p5_instance.redraw();
// =================== THE FIX IS HERE ===================
if (appState.speedGraphInstance) {
// 1. Check if there's data to draw.
if ((appState.canData.length > 0 || appState.vizData) && videoPlayer.duration) {
// 2. Force it to take a new "photograph" with the new theme colors.
appState.speedGraphInstance.drawStaticGraphToBuffer(appState.canData, appState.vizData);
}
// 3. Display the new photograph.
appState.speedGraphInstance.redraw();
// Redraw the speed graph to apply theme changes
if (appState.speedGraphInstance) {
// Check if there's data available to draw on the speed graph
if (
(appState.canData.length > 0 || appState.vizData) &&
videoPlayer.duration
) {
// If data exists, redraw the static parts of the graph to a buffer
// This ensures the background and static elements reflect the new theme
appState.speedGraphInstance.drawStaticGraphToBuffer(
appState.canData,
appState.vizData
);
} }
// ================= END OF FIX =========================
// Request a redraw of the speed graph to display the updated buffer
appState.speedGraphInstance.redraw();
}
} }
export function initializeTheme() { export function initializeTheme() {
const savedTheme = localStorage.getItem('color-theme');
if (savedTheme) {
setTheme(savedTheme);
const savedTheme = localStorage.getItem("color-theme");
if (savedTheme) {
setTheme(savedTheme);
} else {
// Default to light mode if no theme is saved
setTheme("light");
}
themeToggleBtn.addEventListener("click", () => {
if (document.documentElement.classList.contains("dark")) {
setTheme("light");
} else { } else {
// Default to light mode if no theme is saved
setTheme('light');
setTheme("dark");
} }
themeToggleBtn.addEventListener('click', () => {
if (document.documentElement.classList.contains('dark')) {
setTheme('light');
} else {
setTheme('dark');
}
});
});
} }

181
steps/src/utils.js

@ -1,65 +1,126 @@
export function findRadarFrameIndexForTime(targetTimeMs, vizData) { export function findRadarFrameIndexForTime(targetTimeMs, vizData) {
if (!vizData || vizData.radarFrames.length === 0) return -1;
let low = 0, high = vizData.radarFrames.length - 1, ans = 0;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
if (vizData.radarFrames[mid].timestampMs <= targetTimeMs) {
ans = mid; low = mid + 1;
}
else {
high = mid - 1;
}
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;
// 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;
low = mid + 1;
} else {
// If the current frame's timestamp is greater than the target time,
// we need to look in the left half.
high = mid - 1;
} }
return ans;
}
// Return the index of the found radar frame.
return ans;
} }
export function findLastCanIndexBefore(targetTime, canData) { export function findLastCanIndexBefore(targetTime, canData) {
if (!canData || canData.length === 0) return -1;
let low = 0, high = canData.length - 1, ans = -1;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
if (canData[mid].time <= targetTime) {
ans = mid; low = mid + 1;
} else {
high = mid - 1;
// Check for empty or invalid CAN data
if (!canData || canData.length === 0) return -1;
}
// Initialize low, high, and answer variables for binary search
// 'ans' will store the index of the last CAN data point found before the target time
// 'low' and 'high' define the search range
let low = 0,
high = canData.length - 1,
ans = -1; // Initialize ans to -1, indicating no suitable frame found yet.
while (low <= high) {
let mid = Math.floor((low + high) / 2);
if (canData[mid].time <= targetTime) {
ans = mid;
low = mid + 1;
} else {
high = mid - 1;
} }
return ans;
}
// Return the index of the found CAN data point.
return ans;
} }
export function extractTimestampInfo(filename) { export function extractTimestampInfo(filename) {
if (!filename) return null;
let match = filename.match(/Tracks_(\d{8}_\d{6}\.\d{3})/);
if (match) return { timestampStr: match[1], format: 'json' };
match = filename.match(/WIN_(\d{8})_(\d{2})_(\d{2})_(\d{2})/);
if (match) {
const timestamp = `${match[1]}_${match[2]}${match[3]}${match[4]}`;
return { timestampStr: timestamp, format: 'video' };
} match = filename.match(/video_(\d{8}_\d{6})/);
if (match) return {
timestampStr: match[1], format: 'video'
// Return null if filename is not provided
if (!filename) return null;
// Try to match JSON filename pattern: "Tracks_YYYYMMDD_HHMMSS.ms"
// Example: Tracks_20231027_103000.123
let match = filename.match(/Tracks_(\d{8}_\d{6}\.\d{3})/);
if (match) return { timestampStr: match[1], format: "json" };
// Try to match video filename pattern (e.g., from GoPro): "WIN_YYYYMMDD_HH_MM_SS"
// Example: WIN_20231027_10_30_00
match = filename.match(/WIN_(\d{8})_(\d{2})_(\d{2})_(\d{2})/);
if (match) {
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"
// Example: video_20231027_103000
match = filename.match(/video_(\d{8}_\d{6})/);
if (match)
return {
timestampStr: match[1],
format: "video",
}; };
return null;
// If no pattern matches, return null
return null;
} }
export function parseTimestamp(timestampStr, format) { export function parseTimestamp(timestampStr, format) {
if (!timestampStr || !format) return null;
let day, month, year, hour, minute, second, millisecond = 0;
if (format === 'video') {
[year, month, day] = [timestampStr.substring(0, 4), timestampStr.substring(4, 6), timestampStr.substring(6, 8)];
[hour, minute, second] = [timestampStr.substring(9, 11), timestampStr.substring(11, 13), timestampStr.substring(13, 15)];
}
else if (format === 'json') {
[day, month, year] = [timestampStr.substring(0, 2), timestampStr.substring(2, 4), timestampStr.substring(4, 8)];
[hour, minute, second, millisecond] = [timestampStr.substring(9, 11), timestampStr.substring(11, 13), timestampStr.substring(13, 15), parseInt(timestampStr.substring(16, 19))];
}
else {
return null;
}
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
return isNaN(date.getTime()) ? null : date;
// Return null if timestamp string or format is not provided.
if (!timestampStr || !format) return null;
let day,
month,
year,
hour,
minute,
second,
millisecond = 0;
// Parse video timestamp format: YYYYMMDD_HH_MM_SS
// Example: 20231027_10_30_00
if (format === "video") {
[year, month, day] = [
timestampStr.substring(0, 4),
timestampStr.substring(4, 6),
timestampStr.substring(6, 8),
];
[hour, minute, second] = [
timestampStr.substring(9, 11),
timestampStr.substring(11, 13),
timestampStr.substring(13, 15),
];
}
else if (format === "json") {
// Parse JSON timestamp format: DDMMYYYY_HHMMSS.ms
[day, month, year] = [
timestampStr.substring(0, 2),
timestampStr.substring(2, 4),
timestampStr.substring(4, 8),
];
[hour, minute, second, millisecond] = [
timestampStr.substring(9, 11),
timestampStr.substring(11, 13),
timestampStr.substring(13, 15),
parseInt(timestampStr.substring(16, 19)),
];
} else {
// Return null for unsupported formats
return null;
} // Create a Date object using UTC to avoid timezone issues
const date = new Date(
Date.UTC(year, month - 1, day, hour, minute, second, millisecond)
);
// Check if the created Date object is valid.
// If getTime() returns NaN, the date is invalid.
return isNaN(date.getTime()) ? null : date;
} }
/** /**
@ -70,13 +131,19 @@ export function parseTimestamp(timestampStr, format) {
* @returns {Function} Returns the new throttled function. * @returns {Function} Returns the new throttled function.
*/ */
export function throttle(func, delay) { export function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return func(...args);
};
// `lastCall` keeps track of the timestamp of the last successful invocation.
let lastCall = 0;
// Return a new function that, when called, will throttle the execution of the original function
return function (...args) {
// Get the current timestamp.
const now = new Date().getTime();
// If the time since the last call is less than the delay, do not execute the function
if (now - lastCall < delay) {
return;
}
// Otherwise, update the last call time and execute the original function
lastCall = now;
return func(...args); // Apply the original function with its arguments.
};
} }
Loading…
Cancel
Save