Browse Source

Prettier Code and Comments all the way.

refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
eb58f9bf7b
  1. 40
      steps/index.html
  2. 15
      steps/readme.md
  3. 7
      steps/src/constants.js
  4. 60
      steps/src/db.js
  5. 68
      steps/src/dom.js
  6. 311
      steps/src/drawUtils.js
  7. 111
      steps/src/fileParsers.js
  8. 63
      steps/src/main.js
  9. 47
      steps/src/modal.js
  10. 88
      steps/src/p5/radarSketch.js
  11. 177
      steps/src/p5/speedGraphSketch.js
  12. 31
      steps/src/state.js
  13. 40
      steps/src/sync.js
  14. 64
      steps/src/theme.js
  15. 117
      steps/src/utils.js

40
steps/index.html

@ -118,51 +118,40 @@
<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"
<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"
<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"
<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" />
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 />
<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 />
<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"
<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"
<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"
<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" />
id="toggle-debug2-overlay" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Show Advanced Debug</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-close-up"
<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>
@ -192,8 +181,7 @@
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>
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">

15
steps/readme.md

@ -2,9 +2,9 @@ Radar and Video Timestamp VisualizerThis is a high-precision, browser-based tool
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.
-Video: The ground-truth video corresponding to the radar data.
-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.
-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:-
@ -41,8 +40,7 @@ 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:
" 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..
├── index.html # The main HTML shell for the application
@ -64,6 +62,7 @@ Project StructureThe project has been refactored into a modular structure to sep
└── speedGraphSketch.js # The p5.js sketch for the speed graph
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.
- 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.

7
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;
// Frames per second for the video playback.
export const VIDEO_FPS = 30;
// Minimum X-coordinate for the radar plot in meters.
export const RADAR_X_MIN = -20;
// Maximum X-coordinate for the radar plot in meters.
export const RADAR_X_MAX = 20;
// Minimum Y-coordinate for the radar plot in meters.
export const RADAR_Y_MIN = 0;
// Maximum Y-coordinate for the radar plot in meters.
export const RADAR_Y_MAX = 60;

60
steps/src/db.js

@ -1,53 +1,77 @@
// --- IndexedDB for Caching --- //
// -------------------------- IndexedDB for Caching ----------------- //
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) {
const request = indexedDB.open('visualizerDB', 1);
// 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;
if (!db.objectStoreNames.contains('files')) {
db.createObjectStore('files');
// 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();
}; request.onerror = function (event) {
};
// 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) {
// If the database is not initialized, return.
if (!db) return;
const transaction = db.transaction(['files'], 'readwrite');
const store = transaction.objectStore('files');
// 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.`);
request.onerror = (event) => console.error(`Error saving file '${key}':`, event.target.error);
// 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) {
// If the database is not initialized, return.
if (!db) return;
const transaction = db.transaction(['files'], 'readonly');
const store = transaction.objectStore('files'); const request = store.get(key);
// 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 {
} 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 { 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 --- //
@ -65,28 +65,26 @@ export const modalCancelBtn = document.getElementById("modal-cancel-btn");
export const toggleCloseUp = document.getElementById("toggle-close-up");
//----------------------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) {
if (
!appState.vizData ||
frame < 0 ||
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;
timelineSlider.value = appState.currentFrame;
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${
appState.vizData.radarFrames.length
}`;
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.classList.remove("hidden");
} else {
egoSpeedDisplay.classList.add("hidden");
egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display.
}
// --- Start of fix ---
@ -102,14 +100,14 @@ export function updateFrame(frame, forceVideoSeek) {
const offsetMs = parseFloat(offsetInput.value) || 0;
const targetRadarTimeMs = frameData.timestampMs;
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
timeForUpdates = targetVideoTimeSec;
}
timeForUpdates = targetVideoTimeSec; // Update time for subsequent UI updates
}
} // End of forceVideoSeek block
if (!appState.isPlaying) {
// MODIFIED: Use our new synchronized time variable
@ -118,23 +116,23 @@ export function updateFrame(frame, forceVideoSeek) {
}
// --- 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();
}
//----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state.
export function resetVisualization() {
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
const numFrames = appState.vizData.radarFrames.length;
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----------------------//
// Updates the CAN speed display based on the current media time.
export function updateCanDisplay(currentMediaTime) {
if (
appState.canData.length > 0 &&
@ -148,19 +146,19 @@ export function updateCanDisplay(currentMediaTime) {
appState.canData
);
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");
} else {
canSpeedDisplay.classList.add("hidden");
canSpeedDisplay.classList.add("hidden"); // Hide CAN speed display
}
} else {
canSpeedDisplay.classList.add("hidden");
canSpeedDisplay.classList.add("hidden"); // Hide CAN speed display.
}
}
//----------------------DEBUG OVERLAY UPDATE Function----------------------//
// Updates the debug overlay with various synchronization and time information.
export function updateDebugOverlay(currentMediaTime) {
// Check the state of both debug toggles
const isDebug1Visible = toggleDebugOverlay.checked;
@ -168,11 +166,11 @@ export function updateDebugOverlay(currentMediaTime) {
// If neither is checked, hide the overlay and stop
if (!isDebug1Visible && !isDebug2Visible) {
debugOverlay.classList.add("hidden");
debugOverlay.classList.add("hidden"); // Hide debug overlay
return;
}
debugOverlay.classList.remove("hidden");
// If at least one is checked, show the overlay
debugOverlay.classList.remove("hidden"); // Show debug overlay.
let content = [];
// --- Logic for the original debug overlay ---
@ -188,9 +186,9 @@ export function updateDebugOverlay(currentMediaTime) {
.toISOString()
.split("T")[1]
.replace("Z", "")}`
);
); // Format and display video absolute time
} else {
content.push("Video not loaded...");
content.push("Video not loaded..."); // Indicate video not loaded.
}
if (
appState.vizData &&
@ -206,7 +204,7 @@ export function updateDebugOverlay(currentMediaTime) {
.toISOString()
.split("T")[1]
.replace("Z", "")}`
);
); // Format and display radar absolute time
}
}
@ -223,10 +221,10 @@ export function updateDebugOverlay(currentMediaTime) {
const targetRadarTimeMs = currentRadarFrame.timestampMs;
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
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(
`Drift (ms): <b style="color: ${driftColor};">${driftMs.toFixed(0)}</b>`
@ -237,11 +235,11 @@ export function updateDebugOverlay(currentMediaTime) {
content.push(
`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 {
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.
}

311
steps/src/drawUtils.js

@ -3,36 +3,42 @@ import {
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
MAX_TRAJECTORY_LENGTH
} from './constants.js';
import {
appState
} from './state.js';
MAX_TRAJECTORY_LENGTH,
} from "./constants.js";
import { appState } from "./state.js";
import {
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleFrameNorm,
toggleVelocity,
toggleStationaryColor
} from './dom.js';
toggleStationaryColor,
} from "./dom.js";
// Color definitions moved from the sketch
// Defines a set of SNR (Signal-to-Noise Ratio) colors.
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) => [
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 movingColor = (p) => p.color(255, 0, 255); // Magenta
@ -44,16 +50,34 @@ export const movingColor = (p) => p.color(255, 0, 255); // Magenta
export function drawStaticRegionsToBuffer(p, b, plotScales) {
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;
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);
// 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();
}
@ -65,29 +89,62 @@ export function drawStaticRegionsToBuffer(p, b, plotScales) {
*/
export function drawAxes(p, plotScales) {
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);
// 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);
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.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.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.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();
}
// Draw X-axis labels.
for (let x = -15; x <= 15; x += 5) {
if (x === 0) continue;
p.push();
@ -107,16 +164,18 @@ export function drawAxes(p, plotScales) {
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawPointCloud(p, points, plotScales) {
// 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,
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);
const snrVals = points.map((p) => p.snr).filter((snr) => snr !== null);
if (snrVals.length > 1) {
minSnr = Math.min(...snrVals);
maxSnr = Math.max(...snrVals);
@ -125,26 +184,62 @@ export function drawPointCloud(p, points, plotScales) {
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);
// 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);
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);
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);
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);
}
@ -159,21 +254,38 @@ export function drawPointCloud(p, points, plotScales) {
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawTrajectories(p, plotScales) {
// Iterate through each tracked object.
for (const track of appState.vizData.tracks) {
const logs = track.historyLog.filter(log => log.frameIdx <= appState.currentFrame + 1);
// 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];
if (appState.currentFrame + 1 - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH) continue;
// 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;
let trajPts = logs.filter(log => log.correctedPosition && log.correctedPosition[0] !== null).map(log => log.correctedPosition);
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) {
@ -181,11 +293,19 @@ export function drawTrajectories(p, plotScales) {
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));
// 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);
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();
@ -200,20 +320,30 @@ export function drawTrajectories(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);
// 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) {
const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
// 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 : log.predictedPosition;
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) {
@ -221,11 +351,13 @@ export function drawTrackMarkers(p, plotScales) {
p.noFill();
p.rectMode(p.CENTER);
p.square(x, y, size * 1.5);
velocityColor = localStationaryColor;
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);
@ -234,17 +366,33 @@ export function drawTrackMarkers(p, plotScales) {
}
p.pop();
if (showDetails && log.predictedVelocity && log.predictedVelocity[0] !== null) {
// 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.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);
const ttc = (log.ttc !== null && isFinite(log.ttc) && log.ttc < 100) ? `TTC: ${log.ttc.toFixed(1)}s` : '';
// 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);
@ -265,57 +413,66 @@ export function drawTrackMarkers(p, plotScales) {
* @param {object} plotScales - The calculated scales for plotting.
*/
export function handleCloseUpDisplay(p, plotScales) {
// 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;
const screenX = (pt.x * plotScales.plotScaleX) + p.width / 2;
const screenY = p.height * 0.95 - (pt.y * plotScales.plotScaleY);
// 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
screenY: screenY,
});
}
}
// 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);
p.push();
p.textSize(12);
const lineHeight = 15;
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}`;
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);
} // 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);
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++) {
@ -325,21 +482,35 @@ export function handleCloseUpDisplay(p, plotScales) {
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.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);
// 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);
const textColor = document.documentElement.classList.contains('dark') ? p.color(230) : p.color(20);
// 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.text(
infoStrings[i],
boxX + boxPadding,
boxY + boxPadding + i * lineHeight
);
}
p.pop();

111
steps/src/fileParsers.js

@ -1,88 +1,139 @@
//--------------------CAN-LOG PARSER------------------------//
export function processCanLog(logContent, videoStartDate) {
// The function receives everything it needs as arguments.
// It no longer looks at the global state.
// 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 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 };
// 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');
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';
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) {
const [h, m, s, ms] = [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4].substring(0, 3))];
// 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);
const dataBytes = match[6].trim().split(/\s+/).map(hex => parseInt(hex, 16));
// 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.
// 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.
// The processed CAN data is returned under the 'data' key.
return { data: canData };
}
//--------------------JSON PARSER------------------------//
// Add this new function to src/fileParsers.js
export function parseVisualizationJson(jsonString, radarStartTimeMs, videoStartDate) {
export function parseVisualizationJson(
jsonString,
radarStartTimeMs,
videoStartDate
) {
try {
const cleanJsonString = jsonString.replace(/\b(Infinity|NaN|-Infinity)\b/gi, 'null');
// 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.' };
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();
// 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;
vizData.radarFrames.forEach(frame => {
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 => {
frame.pointCloud.forEach((p) => {
// Collect SNR values, ignoring nulls.
if (p.snr !== null) snrValues.push(p.snr);
});
}
});
// 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.');
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 };
return {
error:
"Error parsing JSON file. Please check file format. Error: " +
error.message,
};
}
}

63
steps/src/main.js

@ -40,7 +40,7 @@ import {
findLastCanIndexBefore,
extractTimestampInfo,
parseTimestamp,
throttle
throttle,
} from "./utils.js";
// import state machine from './src/state.js';
import { appState } from "./state.js";
@ -92,19 +92,21 @@ import {
updateCanDisplay,
updateDebugOverlay,
} 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 initialize theme from './src/theme.js';
// Import theme initialization from './src/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";
// Sets up the video player with the given file URL.
function setupVideoPlayer(fileURL) {
videoPlayer.src = fileURL;
videoPlayer.classList.remove("hidden");
videoPlaceholder.classList.add("hidden");
videoPlayer.playbackRate = parseFloat(speedSlider.value);
}
// Event listener for loading JSON file.
loadJsonBtn.addEventListener("click", () => jsonFileInput.click());
loadVideoBtn.addEventListener("click", () => videoFileInput.click());
loadCanBtn.addEventListener("click", () => canFileInput.click());
@ -116,6 +118,7 @@ clearCacheBtn.addEventListener("click", async () => {
window.location.reload();
}
});
// Event listener for JSON file input change.
jsonFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
@ -174,6 +177,7 @@ jsonFileInput.addEventListener("change", (event) => {
};
reader.readAsText(file);
});
// Event listener for video file input change.
videoFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
@ -222,7 +226,7 @@ videoFileInput.addEventListener("change", (event) => {
}
};
});
// Event listener for CAN file input change.
canFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
@ -269,10 +273,12 @@ canFileInput.addEventListener("change", (event) => {
};
reader.readAsText(file);
});
// Event listener for offset input change.
offsetInput.addEventListener("input", () => {
autoOffsetIndicator.classList.add("hidden");
localStorage.setItem("visualizerOffset", offsetInput.value);
});
// Event listener for apply SNR button click.
applySnrBtn.addEventListener("click", () => {
const newMin = parseFloat(snrMinInput.value),
newMax = parseFloat(snrMaxInput.value);
@ -291,6 +297,7 @@ applySnrBtn.addEventListener("click", () => {
appState.p5_instance.redraw();
}
});
// Event listener for play/pause button click.
playPauseBtn.addEventListener("click", () => {
if (!appState.vizData && !videoPlayer.src) return;
appState.isPlaying = !appState.isPlaying;
@ -307,6 +314,7 @@ playPauseBtn.addEventListener("click", () => {
if (videoPlayer.src) videoPlayer.pause();
}
});
// Event listener for stop button click.
stopBtn.addEventListener("click", () => {
videoPlayer.pause();
appState.isPlaying = false;
@ -318,7 +326,10 @@ stopBtn.addEventListener("click", () => {
}
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
});
timelineSlider.addEventListener('input', throttle((event) => {
// Event listener for timeline slider input.
timelineSlider.addEventListener(
"input",
throttle((event) => {
if (!appState.vizData) return;
if (appState.isPlaying) {
videoPlayer.pause();
@ -329,7 +340,10 @@ timelineSlider.addEventListener('input', throttle((event) => {
updateFrame(frame, true);
appState.mediaTimeStart = videoPlayer.currentTime;
appState.masterClockStart = performance.now();
}, 16 )); // 50ms throttle delay
}, 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) => {
const speed = parseFloat(event.target.value);
videoPlayer.playbackRate = speed;
@ -337,6 +351,7 @@ speedSlider.addEventListener("input", (event) => {
});
// ADD THE NEW TOGGLE TO THE ARRAY
// Array of color toggles.
const colorToggles = [
toggleSnrColor,
toggleClusterColor,
@ -353,14 +368,14 @@ colorToggles.forEach((t) => {
if (appState.p5_instance) appState.p5_instance.redraw();
});
});
// Event listeners for various feature toggles.
[
toggleVelocity,
toggleEgoSpeed,
toggleFrameNorm,
toggleTracks,
toggleDebugOverlay,
toggleDebug2Overlay
toggleDebug2Overlay,
].forEach((t) => {
t.addEventListener("change", () => {
if (appState.p5_instance) {
@ -371,10 +386,12 @@ colorToggles.forEach((t) => {
);
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", () => {
appState.isCloseUpMode = toggleCloseUp.checked;
if (appState.p5_instance) {
@ -389,11 +406,12 @@ toggleCloseUp.addEventListener("change", () => {
}
}
});
// Event listener for video ended event.
videoPlayer.addEventListener("ended", () => {
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
});
// Event listener for keyboard arrow key presses to navigate frames.
document.addEventListener("keydown", (event) => {
if (
!appState.vizData ||
@ -420,6 +438,7 @@ document.addEventListener("keydown", (event) => {
appState.masterClockStart = performance.now();
}
});
// Calculates and sets the time offset between JSON and video timestamps.
function calculateAndSetOffset() {
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
@ -453,10 +472,10 @@ function calculateAndSetOffset() {
}
}
// --- Application Initialization ---
// Application Initialization: Event listener for DOMContentLoaded.
document.addEventListener("DOMContentLoaded", () => {
initializeTheme();
console.log("DEBUG: DOMContentLoaded fired. Starting session load.");
console.log("DEBUG: DOMContentLoaded fired. Starting session load."); // Log for debugging.
initDB(() => {
console.log("DEBUG: Database initialized.");
@ -469,8 +488,9 @@ document.addEventListener("DOMContentLoaded", () => {
appState.canLogFilename = localStorage.getItem("canLogFilename");
// 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) =>
loadFileFromDB("video", resolve)
);
@ -480,7 +500,7 @@ document.addEventListener("DOMContentLoaded", () => {
const canLogPromise = new Promise((resolve) =>
loadFileFromDB("canLogText", resolve)
);
// Once all files are loaded from DB, process them.
Promise.all([videoPromise, jsonPromise, canLogPromise])
.then(([videoBlob, jsonString, canLogText]) => {
console.log("DEBUG: All data fetched from IndexedDB.");
@ -488,7 +508,7 @@ document.addEventListener("DOMContentLoaded", () => {
const processAllData = () => {
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) {
const result = parseVisualizationJson(
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) {
const result = processCanLog(canLogText, appState.videoStartDate);
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) {
resetVisualization();
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) {
const fileURL = URL.createObjectURL(videoBlob);
setupVideoPlayer(fileURL);
// This ensures we ONLY process data once the video's duration is known.
videoPlayer.onloadedmetadata = processAllData;
} 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();
}
})

47
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 --- //
// 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 => {
return new Promise((resolve) => {
// Set the message text for the modal.
modalText.textContent = message;
modalCancelBtn.classList.toggle('hidden', !isConfirm);
modalContainer.classList.remove('hidden');
// 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);
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');
modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95");
setTimeout(() => {
modalContainer.classList.add('hidden');
modalContainer.classList.add("hidden");
if (modalResolve) modalResolve(value);
}, 200);
}
//----------------------Modal Event Listeners----------------------//
modalOkBtn.addEventListener('click', () => hideModal(true));
modalCancelBtn.addEventListener('click', () => hideModal(false));
modalOverlay.addEventListener('click', () => hideModal(false));
// 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));

88
steps/src/p5/radarSketch.js

@ -1,47 +1,54 @@
import {
appState
} from '../state.js';
import { appState } from "../state.js";
import {
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';
RADAR_Y_MIN,
} from "../constants.js";
import { canvasContainer, toggleSnrColor, toggleTracks } from "../dom.js";
import {
drawStaticRegionsToBuffer,
drawAxes,
drawPointCloud,
// Import drawing utility functions
drawTrajectories,
drawTrackMarkers,
snrColors,
handleCloseUpDisplay // BUG FIX 1: Import the close-up handler
} from '../drawUtils.js';
handleCloseUpDisplay, // BUG FIX 1: Import the close-up handler
} from "../drawUtils.js";
export const radarSketch = function (p) {
// Object to store calculated plot scales
let plotScales = {
plotScaleX: 1,
plotScaleY: 1
plotScaleY: 1,
};
// p5.Graphics buffers for static elements to optimize drawing
let staticBackgroundBuffer, snrLegendBuffer;
// 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);
}
p.setup = function () {
let canvas = p.createCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
canvas.parent('canvas-container');
// 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);
@ -49,27 +56,41 @@ export const radarSketch = function(p) {
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
p.noLoop();
// Disable continuous looping, redraw will be called manually
};
p.draw = function () {
p.background(document.documentElement.classList.contains('dark') ? p.color(55, 65, 81) : 255);
// 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;
// Draw the pre-rendered static background elements
p.image(staticBackgroundBuffer, 0, 0);
// 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);
// Recalculate plot scales (important for window resizing)
calculatePlotScales();
// Draw coordinate axes
drawAxes(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();
@ -79,12 +100,15 @@ export const radarSketch = function(p) {
handleCloseUpDisplay(p, plotScales);
}
// 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();
@ -92,28 +116,48 @@ export const radarSketch = function(p) {
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, (amt - 0.75) / 0.25);
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);
}
b.fill(document.documentElement.classList.contains('dark') ? 255 : 0);
// 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();
};
// Handle window resizing event
p.windowResized = function () {
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
// BUG FIX 2: Re-create the buffer instead of resizing it

177
steps/src/p5/speedGraphSketch.js

@ -1,41 +1,52 @@
//---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) {
// 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 };
/**
* 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');
const isDark = document.documentElement.classList.contains("dark");
b.background(isDark ? [55, 65, 81] : 255);
const gridColor = isDark ? 100 : 200;
const textColor = isDark ? 200 : 100;
const textColor = isDark ? 200 : 100; // Determine text color based on theme.
// 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);
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);
@ -49,27 +60,54 @@ export const speedGraphSketch = function (p) {
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); }
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();
// Draw CAN speed data line if available.
if (canSpeedData && canSpeedData.length > 0) {
b.noFill();
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();
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.
// Draw radar ego speed data line if available.
if (radarData && radarData.radarFrames) {
b.stroke(0, 200, 100);
b.drawingContext.setLineDash([5, 5]);
@ -77,16 +115,29 @@ export const speedGraphSketch = function (p) {
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 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);
const y = b.map(
egoSpeedKmh,
minSpeed,
maxSpeed,
b.height - pad.bottom,
pad.top
);
b.vertex(x, y);
}
}
b.endShape();
b.drawingContext.setLineDash([]);
}
b.drawingContext.setLineDash([]); // Reset line dash to solid.
} // End of radar ego speed data drawing.
// Draw legend for the graph lines.
b.push();
b.strokeWeight(2);
b.noStroke();
@ -104,61 +155,105 @@ export const speedGraphSketch = function (p) {
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');
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;
if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return; // Exit if no data.
videoDuration = duration;
let speeds = [];
if (canSpeedData) {
speeds.push(...canSpeedData.map(d => parseFloat(d.speed)));
speeds.push(...canSpeedData.map((d) => parseFloat(d.speed)));
}
if (radarData && radarData.radarFrames) {
const egoSpeeds = radarData.radarFrames.map(frame => frame.egoVelocity[1] * 3.6);
const egoSpeeds = radarData.radarFrames.map(
(frame) => frame.egoVelocity[1] * 3.6
);
speeds.push(...egoSpeeds);
}
minSpeed = speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0;
maxSpeed = speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 10;
// 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;
// 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;
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);
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);
const videoAbsTimeMs = appState.videoStartDate.getTime() + (currentTime * 1000);
// 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);
const y = p.map(
canMsg.speed,
minSpeed,
maxSpeed,
p.height - pad.bottom,
pad.top
);
p.fill(255, 0, 0);
p.noStroke();
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);
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

31
steps/src/state.js

@ -1,19 +1,38 @@
export const appState = {
// 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,
globalMinSnr : 0, globalMaxSnr : 1,
p5_instance : null, speedGraphInstance : null,
jsonFilename : '', videoFilename : '', canLogFilename : '',
// 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,
};

40
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.
@ -10,16 +18,28 @@ import { findRadarFrameIndexForTime } from './utils.js';
export function animationLoop() {
if (!appState.isPlaying) return;
// Get the current playback speed from the slider
const playbackSpeed = parseFloat(speedSlider.value);
// Calculate the elapsed real time since the master clock started
const elapsedRealTime = performance.now() - appState.masterClockStart;
const currentMediaTime = appState.mediaTimeStart + (elapsedRealTime / 1000) * playbackSpeed;
// Calculate the current media time based on the master clock, initial media time, elapsed real time, and playback speed
const currentMediaTime =
appState.mediaTimeStart + (elapsedRealTime / 1000) * playbackSpeed;
// Update radar frame based on the master clock
// Check if visualization data and video start date are available
if (appState.vizData && appState.videoStartDate) {
// Get the offset from the input field, default to 0 if not a valid number
const offsetMs = parseFloat(offsetInput.value) || 0;
const targetRadarTimeMs = (currentMediaTime * 1000);
const targetFrame = findRadarFrameIndexForTime(targetRadarTimeMs, appState.vizData);
// 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);
}
}
@ -29,7 +49,9 @@ export function animationLoop() {
if (now - appState.lastSyncTime > 500) {
const videoTime = videoPlayer.currentTime;
const drift = Math.abs(currentMediaTime - videoTime);
if (drift > 0.15) { // Resync if drift is > 150ms
// 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;
}
@ -42,9 +64,11 @@ export function animationLoop() {
return;
}
// Update other UI elements
// 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

64
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) {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
lightIcon.classList.remove('hidden');
darkIcon.classList.add('hidden');
localStorage.setItem('color-theme', 'dark');
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');
document.documentElement.classList.remove("dark");
darkIcon.classList.remove("hidden");
lightIcon.classList.add("hidden");
localStorage.setItem("color-theme", "light");
}
// Redraw the main radar plot
// Redraw the main radar plot to apply theme changes
if (appState.p5_instance) appState.p5_instance.redraw();
// =================== THE FIX IS HERE ===================
// Redraw the speed graph to apply theme changes
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);
// 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
);
}
// 3. Display the new photograph.
// Request a redraw of the speed graph to display the updated buffer
appState.speedGraphInstance.redraw();
}
// ================= END OF FIX =========================
}
export function initializeTheme() {
const savedTheme = localStorage.getItem('color-theme');
const savedTheme = localStorage.getItem("color-theme");
if (savedTheme) {
setTheme(savedTheme);
} else {
// Default to light mode if no theme is saved
setTheme('light');
setTheme("light");
}
themeToggleBtn.addEventListener('click', () => {
if (document.documentElement.classList.contains('dark')) {
setTheme('light');
themeToggleBtn.addEventListener("click", () => {
if (document.documentElement.classList.contains("dark")) {
setTheme("light");
} else {
setTheme('dark');
setTheme("dark");
}
});
}

117
steps/src/utils.js

@ -1,64 +1,125 @@
export function findRadarFrameIndexForTime(targetTimeMs, vizData) {
if (!vizData || vizData.radarFrames.length === 0) return -1;
let low = 0, high = vizData.radarFrames.length - 1, ans = 0;
// 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 {
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 the index of the found radar frame.
return ans;
}
export function findLastCanIndexBefore(targetTime, canData) {
// Check for empty or invalid CAN data
if (!canData || canData.length === 0) return -1;
let low = 0, high = canData.length - 1, ans = -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;
ans = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
// Return the index of the found CAN data point.
return ans;
}
export function extractTimestampInfo(filename) {
// 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' };
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' };
} match = filename.match(/video_(\d{8}_\d{6})/);
if (match) return {
timestampStr: match[1], format: 'video'
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",
};
// If no pattern matches, return null
return null;
}
export function parseTimestamp(timestampStr, format) {
// Return null if timestamp string or format is not provided.
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)];
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') {
[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 {
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;
}
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
} // 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.
*/
export function throttle(func, delay) {
// `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);
return func(...args); // Apply the original function with its arguments.
};
}
Loading…
Cancel
Save