Browse Source

OH MY GOD. What happened over here. Everything is back to its original state now, also we are using CAN speed data from JSON. CAN LOAD button removed.

refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
f560d5fa5e
  1. 75
      steps/src/dom.js
  2. 34
      steps/src/drawUtils.js
  3. 70
      steps/src/fileParsers.js
  4. 147
      steps/src/main.js
  5. 179
      steps/src/p5/speedGraphSketch.js
  6. 6
      steps/src/state.js
  7. 3
      steps/src/sync.js
  8. 19
      steps/src/theme.js
  9. 21
      steps/src/utils.js

75
steps/src/dom.js

@ -1,5 +1,4 @@
import { appState } from "./state.js"; import { appState } from "./state.js";
import { findLastCanIndexBefore } from "./utils.js";
import { VIDEO_FPS } from "./constants.js"; // Import VIDEO_FPS for debug overlay calculations import { VIDEO_FPS } from "./constants.js"; // Import VIDEO_FPS for debug overlay calculations
// --- DOM Element References --- // // --- DOM Element References --- //
@ -23,25 +22,39 @@ export const speedSlider = document.getElementById("speed-slider");
export const speedDisplay = document.getElementById("speed-display"); export const speedDisplay = document.getElementById("speed-display");
export const featureToggles = document.getElementById("feature-toggles"); export const featureToggles = document.getElementById("feature-toggles");
export const toggleSnrColor = document.getElementById("toggle-snr-color"); export const toggleSnrColor = document.getElementById("toggle-snr-color");
export const toggleClusterColor = document.getElementById("toggle-cluster-color");
export const toggleClusterColor = document.getElementById(
"toggle-cluster-color"
);
export const toggleInlierColor = document.getElementById("toggle-inlier-color"); export const toggleInlierColor = document.getElementById("toggle-inlier-color");
export const toggleStationaryColor = document.getElementById("toggle-stationary-color");
export const toggleStationaryColor = document.getElementById(
"toggle-stationary-color"
);
export const toggleVelocity = document.getElementById("toggle-velocity"); export const toggleVelocity = document.getElementById("toggle-velocity");
export const toggleTracks = document.getElementById("toggle-tracks"); export const toggleTracks = document.getElementById("toggle-tracks");
export const toggleEgoSpeed = document.getElementById("toggle-ego-speed"); export const toggleEgoSpeed = document.getElementById("toggle-ego-speed");
export const toggleFrameNorm = document.getElementById("toggle-frame-norm"); export const toggleFrameNorm = document.getElementById("toggle-frame-norm");
export const toggleDebugOverlay = document.getElementById("toggle-debug-overlay");
export const toggleDebugOverlay = document.getElementById(
"toggle-debug-overlay"
);
export const egoSpeedDisplay = document.getElementById("ego-speed-display"); export const egoSpeedDisplay = document.getElementById("ego-speed-display");
export const canSpeedDisplay = document.getElementById("can-speed-display"); export const canSpeedDisplay = document.getElementById("can-speed-display");
export const debugOverlay = document.getElementById("debug-overlay"); export const debugOverlay = document.getElementById("debug-overlay");
export const toggleDebug2Overlay = document.getElementById("toggle-debug2-overlay");
export const toggleDebug2Overlay = document.getElementById(
"toggle-debug2-overlay"
);
export const snrMinInput = document.getElementById("snr-min-input"); export const snrMinInput = document.getElementById("snr-min-input");
export const snrMaxInput = document.getElementById("snr-max-input"); export const snrMaxInput = document.getElementById("snr-max-input");
export const applySnrBtn = document.getElementById("apply-snr-btn"); export const applySnrBtn = document.getElementById("apply-snr-btn");
export const autoOffsetIndicator = document.getElementById("auto-offset-indicator");
export const autoOffsetIndicator = document.getElementById(
"auto-offset-indicator"
);
export const clearCacheBtn = document.getElementById("clear-cache-btn"); export const clearCacheBtn = document.getElementById("clear-cache-btn");
export const speedGraphContainer = document.getElementById("speed-graph-container");
export const speedGraphPlaceholder = document.getElementById("speed-graph-placeholder");
export const speedGraphContainer = document.getElementById(
"speed-graph-container"
);
export const speedGraphPlaceholder = document.getElementById(
"speed-graph-placeholder"
);
export const modalContainer = document.getElementById("modal-container"); export const modalContainer = document.getElementById("modal-container");
export const modalOverlay = document.getElementById("modal-overlay"); export const modalOverlay = document.getElementById("modal-overlay");
export const modalContent = document.getElementById("modal-content"); export const modalContent = document.getElementById("modal-content");
@ -49,7 +62,9 @@ export const modalText = document.getElementById("modal-text");
export const modalOkBtn = document.getElementById("modal-ok-btn"); export const modalOkBtn = document.getElementById("modal-ok-btn");
export const modalCancelBtn = document.getElementById("modal-cancel-btn"); export const modalCancelBtn = document.getElementById("modal-cancel-btn");
export const toggleCloseUp = document.getElementById("toggle-close-up"); export const toggleCloseUp = document.getElementById("toggle-close-up");
export const togglePredictedPos = document.getElementById("toggle-predicted-pos");
export const togglePredictedPos = document.getElementById(
"toggle-predicted-pos"
);
export const toggleCovariance = document.getElementById("toggle-covariance"); export const toggleCovariance = document.getElementById("toggle-covariance");
//----------------------UPDATE FRAME Function----------------------// //----------------------UPDATE FRAME Function----------------------//
@ -77,8 +92,22 @@ export function updateFrame(frame, forceVideoSeek) {
egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display. egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display.
} }
// --- Start of fix ---
let timeForUpdates = videoPlayer.currentTime; // NEW: Default to the video's current time
// --- ADD THIS NEW BLOCK ---
if (
frameData &&
frameData.canVehSpeed_kmph !== null &&
!isNaN(frameData.canVehSpeed_kmph)
) {
canSpeedDisplay.textContent = `CAN: ${frameData.canVehSpeed_kmph.toFixed(
1
)} km/h`;
canSpeedDisplay.classList.remove("hidden");
} else {
canSpeedDisplay.classList.add("hidden");
}
// --- END OF NEW BLOCK ---
let timeForUpdates = videoPlayer.currentTime; // NEW: Default to the video's current time
if ( if (
forceVideoSeek && forceVideoSeek &&
@ -103,7 +132,6 @@ export function updateFrame(frame, forceVideoSeek) {
if (!appState.isPlaying) { if (!appState.isPlaying) {
// MODIFIED: Use our new synchronized time variable // MODIFIED: Use our new synchronized time variable
updateCanDisplay(timeForUpdates);
updateDebugOverlay(timeForUpdates); updateDebugOverlay(timeForUpdates);
} }
// --- End of fix --- // --- End of fix ---
@ -126,29 +154,6 @@ export function resetVisualization() {
//----------------------CAN DISPLAY UPDATE Function----------------------// //----------------------CAN DISPLAY UPDATE Function----------------------//
// Updates the CAN speed display based on the current media time. // Updates the CAN speed display based on the current media time.
export function updateCanDisplay(currentMediaTime) {
if (
appState.canData.length > 0 &&
videoPlayer.src &&
appState.videoStartDate
) {
const videoAbsoluteTimeMs =
appState.videoStartDate.getTime() + currentMediaTime * 1000;
const canIndex = findLastCanIndexBefore(
videoAbsoluteTimeMs,
appState.canData
);
if (canIndex !== -1) {
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"); // Hide CAN speed display
}
} else {
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. // Updates the debug overlay with various synchronization and time information.

34
steps/src/drawUtils.js

@ -525,9 +525,25 @@ export function drawCovarianceEllipse(p, position, covarianceP, plotScales) {
const trace = a + d; const trace = a + d;
const determinant = a * d - b * b; const determinant = a * d - b * b;
const lambda1 = trace / 2 + Math.sqrt(Math.pow(trace, 2) / 4 - determinant);
const lambda2 = trace / 2 - Math.sqrt(Math.pow(trace, 2) / 4 - determinant);
//const lambda1 = trace / 2 + Math.sqrt(Math.pow(trace, 2) / 4 - determinant);
//const lambda2 = trace / 2 - Math.sqrt(Math.pow(trace, 2) / 4 - determinant);
// --- START: New robust calculation with logging ---
let sqrtTermVal = Math.pow(trace, 2) / 4 - determinant;
// Check for a negative value, which causes NaN errors
if (sqrtTermVal < 0) {
// Log a warning so we know it happened, as you suggested
console.warn(
`Clamping negative sqrtTermVal in frame ${appState.currentFrame} to prevent NaN. Original value: ${sqrtTermVal}`
);
// Clamp the value to 0. This allows drawing to continue instead of breaking.
sqrtTermVal = 0;
}
const sqrtTerm = Math.sqrt(sqrtTermVal);
const lambda1 = trace / 2 + sqrtTerm;
const lambda2 = trace / 2 - sqrtTerm;
// --- END: New robust calculation with logging ---
const chi2 = 5.991; const chi2 = 5.991;
const majorAxis = Math.sqrt(chi2 * lambda1); const majorAxis = Math.sqrt(chi2 * lambda1);
const minorAxis = Math.sqrt(chi2 * lambda2); const minorAxis = Math.sqrt(chi2 * lambda2);
@ -542,8 +558,16 @@ export function drawCovarianceEllipse(p, position, covarianceP, plotScales) {
p.noFill(); p.noFill();
p.stroke(255, 0, 0, 150); p.stroke(255, 0, 0, 150);
p.strokeWeight(1); p.strokeWeight(1);
p.translate(position[0] * plotScales.plotScaleX, position[1] * plotScales.plotScaleY);
p.translate(
position[0] * plotScales.plotScaleX,
position[1] * plotScales.plotScaleY
);
p.rotate(angle); p.rotate(angle);
p.ellipse(0, 0, majorAxis * 2 * plotScales.plotScaleX, minorAxis * 2 * plotScales.plotScaleY);
p.ellipse(
0,
0,
majorAxis * 2 * plotScales.plotScaleX,
minorAxis * 2 * plotScales.plotScaleY
);
p.pop(); p.pop();
}
}

70
steps/src/fileParsers.js

@ -37,76 +37,6 @@ export function parseJsonWithOboe(fileURL, onComplete, onError) {
} }
//--------------------CAN-LOG PARSER------------------------//
export function processCanLog(logContent, videoStartDate) {
// 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 });
}
}
}
// 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}.`
);
// It returns the finished product in a structured object.
// The processed CAN data is returned under the 'data' key.
return { data: canData };
}
//--------------------JSON POST-PROCESSOR (ASYNCHRONOUS & SAFE)------------------------// //--------------------JSON POST-PROCESSOR (ASYNCHRONOUS & SAFE)------------------------//
// Helper function to process large arrays in chunks without blocking // Helper function to process large arrays in chunks without blocking

147
steps/src/main.js

@ -19,11 +19,7 @@
import { animationLoop } from "./sync.js"; import { animationLoop } from "./sync.js";
import { radarSketch } from "./p5/radarSketch.js"; import { radarSketch } from "./p5/radarSketch.js";
import { speedGraphSketch } from "./p5/speedGraphSketch.js"; import { speedGraphSketch } from "./p5/speedGraphSketch.js";
import {
processCanLog,
parseVisualizationJson,
parseJsonWithOboe,
} from "./fileParsers.js";
import { parseVisualizationJson, parseJsonWithOboe } from "./fileParsers.js";
import { import {
MAX_TRAJECTORY_LENGTH, MAX_TRAJECTORY_LENGTH,
VIDEO_FPS, VIDEO_FPS,
@ -34,7 +30,6 @@ import {
} from "./constants.js"; } from "./constants.js";
import { import {
findRadarFrameIndexForTime, findRadarFrameIndexForTime,
findLastCanIndexBefore,
extractTimestampInfo, extractTimestampInfo,
parseTimestamp, parseTimestamp,
throttle, throttle,
@ -47,10 +42,8 @@ import {
videoPlaceholder, videoPlaceholder,
loadJsonBtn, loadJsonBtn,
loadVideoBtn, loadVideoBtn,
loadCanBtn,
jsonFileInput, jsonFileInput,
videoFileInput, videoFileInput,
canFileInput,
playPauseBtn, playPauseBtn,
stopBtn, stopBtn,
timelineSlider, timelineSlider,
@ -70,7 +63,6 @@ import {
toggleDebugOverlay, toggleDebugOverlay,
toggleDebug2Overlay, toggleDebug2Overlay,
egoSpeedDisplay, egoSpeedDisplay,
canSpeedDisplay,
debugOverlay, debugOverlay,
snrMinInput, snrMinInput,
snrMaxInput, snrMaxInput,
@ -82,7 +74,6 @@ import {
toggleCloseUp, toggleCloseUp,
updateFrame, updateFrame,
resetVisualization, resetVisualization,
updateCanDisplay,
updateDebugOverlay, updateDebugOverlay,
} from "./dom.js"; } from "./dom.js";
import { showModal } from "./modal.js"; import { showModal } from "./modal.js";
@ -111,11 +102,10 @@ clearCacheBtn.addEventListener("click", async () => {
}); });
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;
appState.jsonFilename = file.name; appState.jsonFilename = file.name;
localStorage.setItem("jsonFilename", appState.jsonFilename); localStorage.setItem("jsonFilename", appState.jsonFilename);
@ -123,55 +113,49 @@ jsonFileInput.addEventListener("change", (event) => {
calculateAndSetOffset(); calculateAndSetOffset();
saveFileToDB("json", file); // Save the file object for the next session saveFileToDB("json", file); // Save the file object for the next session
// 1. Show a loading modal immediately. // 1. Show a loading modal immediately.
showModal("Parsing large JSON file, please wait..."); showModal("Parsing large JSON file, please wait...");
// 2. Create a temporary URL for the streaming parser. // 2. Create a temporary URL for the streaming parser.
const fileURL = URL.createObjectURL(file); const fileURL = URL.createObjectURL(file);
// 3. Use the robust streaming parser. // 3. Use the robust streaming parser.
parseJsonWithOboe( parseJsonWithOboe(
fileURL, fileURL,
async (parsedData) => { async (parsedData) => {
// This is the success callback, running after the file is parsed. // This is the success callback, running after the file is parsed.
// We make it async so we can `await` the next step. // We make it async so we can `await` the next step.
const result = await parseVisualizationJson(
const result = await parseVisualizationJson(
parsedData, parsedData,
appState.radarStartTimeMs, appState.radarStartTimeMs,
appState.videoStartDate appState.videoStartDate
); );
// Revoke the temporary URL to free up memory. // Revoke the temporary URL to free up memory.
URL.revokeObjectURL(fileURL); URL.revokeObjectURL(fileURL);
if (result.error) {
if (result.error) {
showModal(result.error); showModal(result.error);
return; return;
} }
appState.vizData = result.data; appState.vizData = result.data;
appState.globalMinSnr = result.minSnr; appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr; appState.globalMaxSnr = result.maxSnr;
// Update UI with the correct, awaited data. // Update UI with the correct, awaited data.
snrMinInput.value = appState.globalMinSnr.toFixed(1); snrMinInput.value = appState.globalMinSnr.toFixed(1);
@ -183,47 +167,38 @@ jsonFileInput.addEventListener("change", (event) => {
canvasPlaceholder.style.display = "none"; canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden"); featureToggles.classList.remove("hidden");
if (!appState.p5_instance) {
if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch); appState.p5_instance = new p5(radarSketch);
} }
if (appState.speedGraphInstance) {
appState.speedGraphInstance.setData(
appState.canData,
appState.vizData,
videoPlayer.duration
);
if (appState.vizData) {
speedGraphPlaceholder.classList.add("hidden");
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
if (videoPlayer.duration) {
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
}
} }
// Close the loading modal. // Close the loading modal.
document.getElementById("modal-ok-btn").click(); document.getElementById("modal-ok-btn").click();
}, },
(error) => { (error) => {
// This is the error callback for the streaming parser. // This is the error callback for the streaming parser.
showModal(error); showModal(error);
URL.revokeObjectURL(fileURL); URL.revokeObjectURL(fileURL);
} }
); );
}); });
// Event listener for video file input change. // Event listener for video file input change.
videoFileInput.addEventListener("change", (event) => { videoFileInput.addEventListener("change", (event) => {
@ -235,17 +210,6 @@ videoFileInput.addEventListener("change", (event) => {
calculateAndSetOffset(); calculateAndSetOffset();
if (appState.rawCanLogText) {
const result = processCanLog(
appState.rawCanLogText,
appState.videoStartDate
);
if (!result.error) {
appState.canData = result.data;
appState.rawCanLogText = null;
}
}
if (appState.vizData) { if (appState.vizData) {
console.log("DEBUG: Video loaded after JSON. Re-calculating timestamps."); console.log("DEBUG: Video loaded after JSON. Re-calculating timestamps.");
appState.vizData.radarFrames.forEach((frame) => { appState.vizData.radarFrames.forEach((frame) => {
@ -263,7 +227,6 @@ videoFileInput.addEventListener("change", (event) => {
videoPlayer.onloadedmetadata = () => { videoPlayer.onloadedmetadata = () => {
if (appState.speedGraphInstance) { if (appState.speedGraphInstance) {
appState.speedGraphInstance.setData( appState.speedGraphInstance.setData(
appState.canData,
appState.vizData, appState.vizData,
videoPlayer.duration videoPlayer.duration
); );
@ -271,46 +234,6 @@ videoFileInput.addEventListener("change", (event) => {
}; };
}); });
// Event listener for CAN file input change.
appState.canLogFilename = file.name;
localStorage.setItem("canLogFilename", appState.canLogFilename);
const reader = new FileReader();
reader.onload = (e) => {
const logContent = e.target.result;
saveFileToDB("canLogText", logContent);
const result = processCanLog(logContent, appState.videoStartDate);
if (result.error) {
showModal(result.error);
appState.rawCanLogText = result.rawCanLogText;
return;
}
appState.canData = result.data;
appState.rawCanLogText = null;
if (appState.canData.length > 0 || appState.vizData) {
speedGraphPlaceholder.classList.add("hidden");
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
if (videoPlayer.duration) {
appState.speedGraphInstance.setData(
appState.canData,
appState.vizData,
videoPlayer.duration
);
}
} else {
showModal(`No CAN messages with ID 0x30F found.`);
}
};
reader.readAsText(file);
// Event listener for offset input change. // Event listener for offset input change.
offsetInput.addEventListener("input", () => { offsetInput.addEventListener("input", () => {
autoOffsetIndicator.classList.add("hidden"); autoOffsetIndicator.classList.add("hidden");
@ -525,7 +448,6 @@ document.addEventListener("DOMContentLoaded", () => {
} }
appState.videoFilename = localStorage.getItem("videoFilename"); appState.videoFilename = localStorage.getItem("videoFilename");
appState.jsonFilename = localStorage.getItem("jsonFilename"); appState.jsonFilename = localStorage.getItem("jsonFilename");
appState.canLogFilename = localStorage.getItem("canLogFilename");
calculateAndSetOffset(); calculateAndSetOffset();
@ -535,14 +457,11 @@ document.addEventListener("DOMContentLoaded", () => {
const jsonPromise = new Promise((resolve) => const jsonPromise = new Promise((resolve) =>
loadFileFromDB("json", resolve) loadFileFromDB("json", resolve)
); );
const canLogPromise = new Promise((resolve) =>
loadFileFromDB("canLogText", resolve)
);
// At the end of main.js, inside the DOMContentLoaded listener // At the end of main.js, inside the DOMContentLoaded listener
Promise.all([videoPromise, jsonPromise, canLogPromise])
.then(([videoBlob, jsonBlob, canLogText]) => {
Promise.all([videoPromise, jsonPromise])
.then(([videoBlob, jsonBlob]) => {
// Renamed jsonString to jsonBlob // Renamed jsonString to jsonBlob
console.log("DEBUG: All data fetched from IndexedDB."); console.log("DEBUG: All data fetched from IndexedDB.");
@ -565,10 +484,6 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
if (canLogText && appState.videoStartDate) {
// ... (process CAN log)
}
// Final UI updates // Final UI updates
if (appState.vizData) { if (appState.vizData) {
resetVisualization(); resetVisualization();
@ -578,9 +493,17 @@ document.addEventListener("DOMContentLoaded", () => {
appState.p5_instance = new p5(radarSketch); appState.p5_instance = new p5(radarSketch);
} }
} }
if (appState.canData.length > 0 || appState.vizData) {
if (appState.vizData) {
speedGraphPlaceholder.classList.add("hidden"); speedGraphPlaceholder.classList.add("hidden");
// ... (rest of the UI update logic)
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
if (videoPlayer.duration) {
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
}
} }
}; };

179
steps/src/p5/speedGraphSketch.js

@ -1,37 +1,20 @@
//---Import APPSTATE VIDEOPLAYER and FindLastCanIndex---//
import { appState } from "../state.js"; import { appState } from "../state.js";
import { videoPlayer, speedGraphContainer } from "../dom.js"; import { videoPlayer, speedGraphContainer } from "../dom.js";
import { findLastCanIndexBefore } from "../utils.js";
export const speedGraphSketch = function (p) { export const speedGraphSketch = function (p) {
// Declare variables for the static buffer, min/max speed for scaling, and video duration.
let staticBuffer, minSpeed, maxSpeed, videoDuration; 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 }; 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) {
p.drawStaticGraphToBuffer = function (radarData) {
const b = staticBuffer; const b = staticBuffer;
b.clear(); b.clear();
const isDark = document.documentElement.classList.contains("dark"); const isDark = document.documentElement.classList.contains("dark");
b.background(isDark ? [55, 65, 81] : 255); b.background(isDark ? [55, 65, 81] : 255);
const gridColor = isDark ? 100 : 200; const gridColor = isDark ? 100 : 200;
const textColor = isDark ? 200 : 100; // Determine text color based on theme.
const textColor = isDark ? 200 : 100;
// Push current drawing style settings onto a stack.
b.push(); b.push();
// Set stroke for grid lines.
b.stroke(gridColor); b.stroke(gridColor);
// Set stroke weight for grid lines.
b.strokeWeight(1); b.strokeWeight(1);
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom); b.line(pad.left, pad.top, pad.left, b.height - pad.bottom);
b.line( b.line(
@ -39,14 +22,11 @@ export const speedGraphSketch = function (p) {
b.height - pad.bottom, b.height - pad.bottom,
b.width - pad.right, b.width - pad.right,
b.height - pad.bottom b.height - pad.bottom
); // Draw Y and X axes.
// Set text alignment for Y-axis labels.
);
b.textAlign(b.RIGHT, b.CENTER); b.textAlign(b.RIGHT, b.CENTER);
b.noStroke(); b.noStroke();
b.fill(textColor); b.fill(textColor);
// Set text size for labels.
b.textSize(10); b.textSize(10);
// Draw horizontal grid lines and speed labels.
for (let s = minSpeed; s <= maxSpeed; s += 10) { for (let s = minSpeed; s <= maxSpeed; s += 10) {
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.text(s, pad.left - 8, y); b.text(s, pad.left - 8, y);
@ -60,14 +40,11 @@ export const speedGraphSketch = function (p) {
b.line(pad.left + 1, y, b.width - pad.right, y); b.line(pad.left + 1, y, b.width - pad.right, y);
b.noStroke(); b.noStroke();
} }
// Draw Y-axis unit label.
b.fill(textColor); b.fill(textColor);
b.text("km/h", pad.left - 8, pad.top - 8); b.text("km/h", pad.left - 8, pad.top - 8);
// Set text alignment for X-axis labels.
b.textAlign(b.CENTER, b.TOP); b.textAlign(b.CENTER, b.TOP);
b.noStroke(); b.noStroke();
b.fill(isDark ? 180 : 150); b.fill(isDark ? 180 : 150);
// Calculate time interval for X-axis labels.
const tInt = Math.max(1, Math.floor(videoDuration / 10)); const tInt = Math.max(1, Math.floor(videoDuration / 10));
for (let t = 0; t <= videoDuration; t += tInt) { for (let t = 0; t <= videoDuration; t += tInt) {
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right); const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right);
@ -75,39 +52,27 @@ export const speedGraphSketch = function (p) {
} }
b.fill(textColor); b.fill(textColor);
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18); b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18);
// Restore previous drawing style settings.
b.pop(); b.pop();
// Draw CAN speed data line if available.
if (canSpeedData && canSpeedData.length > 0) {
b.noFill(); // Do not fill the shape.
b.stroke(0, 150, 255);
if (radarData && radarData.radarFrames) {
b.noFill();
b.stroke(0, 150, 255); // Blue for CAN speed
b.strokeWeight(1.5); b.strokeWeight(1.5);
b.beginShape(); b.beginShape();
for (const d of canSpeedData) {
const relTime = (d.time - appState.videoStartDate.getTime()) / 1000;
for (const frame of radarData.radarFrames) {
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) {
continue;
}
const relTime = frame.timestampMs / 1000;
if (relTime >= 0 && relTime <= videoDuration) { 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
);
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right);
const y = b.map(frame.canVehSpeed_kmph, minSpeed, maxSpeed, b.height - pad.bottom, pad.top);
b.vertex(x, y); b.vertex(x, y);
} }
} }
b.endShape(); b.endShape();
} // End of CAN speed data drawing.
}
// Draw radar ego speed data line if available.
if (radarData && radarData.radarFrames) { if (radarData && radarData.radarFrames) {
b.stroke(0, 200, 100); b.stroke(0, 200, 100);
b.drawingContext.setLineDash([5, 5]); b.drawingContext.setLineDash([5, 5]);
@ -115,29 +80,16 @@ export const speedGraphSketch = function (p) {
for (const frame of radarData.radarFrames) { for (const frame of radarData.radarFrames) {
const relTime = frame.timestampMs / 1000; const relTime = frame.timestampMs / 1000;
if (relTime >= 0 && relTime <= videoDuration) { 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 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.vertex(x, y);
} }
} }
b.endShape(); b.endShape();
b.drawingContext.setLineDash([]); // Reset line dash to solid.
} // End of radar ego speed data drawing.
b.drawingContext.setLineDash([]);
}
// Draw legend for the graph lines.
b.push(); b.push();
b.strokeWeight(2); b.strokeWeight(2);
b.noStroke(); b.noStroke();
@ -155,111 +107,88 @@ export const speedGraphSketch = function (p) {
b.text("Ego Speed", b.width - 95, pad.top + 30); b.text("Ego Speed", b.width - 95, pad.top + 30);
b.pop(); b.pop();
}; };
/**
* p5.js setup function. Initializes the canvas and static buffer.
*/
p.setup = function () { p.setup = function () {
let canvas = p.createCanvas( let canvas = p.createCanvas(
speedGraphContainer.offsetWidth, speedGraphContainer.offsetWidth,
speedGraphContainer.offsetHeight speedGraphContainer.offsetHeight
); );
canvas.parent("speed-graph-container"); canvas.parent("speed-graph-container");
// Create an off-screen graphics buffer for static elements.
staticBuffer = p.createGraphics(p.width, p.height); staticBuffer = p.createGraphics(p.width, p.height);
// Disable continuous looping; draw will be called manually.
p.noLoop(); 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.
p.setData = function (radarData, duration) {
if (!radarData || !radarData.radarFrames) return;
videoDuration = duration; videoDuration = duration;
let speeds = []; let speeds = [];
if (canSpeedData) {
speeds.push(...canSpeedData.map((d) => parseFloat(d.speed)));
}
if (radarData && radarData.radarFrames) { if (radarData && radarData.radarFrames) {
const egoSpeeds = radarData.radarFrames.map( const egoSpeeds = radarData.radarFrames.map(
(frame) => frame.egoVelocity[1] * 3.6 (frame) => frame.egoVelocity[1] * 3.6
); );
speeds.push(...egoSpeeds); speeds.push(...egoSpeeds);
const canSpeeds = radarData.radarFrames
.map((frame) => frame.canVehSpeed_kmph)
.filter((speed) => speed !== null && !isNaN(speed));
speeds.push(...canSpeeds);
} }
// 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.
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 (maxSpeed <= 0) maxSpeed = 10;
// Ensure minSpeed is 0 if all speeds are non-negative.
if (minSpeed >= 0) minSpeed = 0; 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.drawStaticGraphToBuffer(radarData);
p.redraw(); p.redraw();
}; };
/**
* p5.js draw function. Draws the static buffer and the dynamic time indicator.
*/
p.draw = function () { p.draw = function () {
if (!videoDuration) return; // Only draw if video duration is set.
if (!videoDuration) return;
p.image(staticBuffer, 0, 0); p.image(staticBuffer, 0, 0);
drawTimeIndicator(); drawTimeIndicator();
}; };
function drawTimeIndicator() { function drawTimeIndicator() {
const currentTime = videoPlayer.currentTime;
// Get the current frame's data as the single source of truth
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return; // Exit if data isn't ready
// Calculate the X position from the current frame's precise timestamp
const currentTimeSec = frameData.timestampMs / 1000.0;
const x = p.map( const x = p.map(
currentTime,
currentTimeSec,
0, 0,
videoDuration, videoDuration,
pad.left, pad.left,
p.width - pad.right p.width - pad.right
); // Map current time to X-coordinate.
// Draw the red time indicator line.
);
// Draw the red time indicator line at the accurate X position
p.stroke(255, 0, 0, 150); p.stroke(255, 0, 0, 150);
p.strokeWeight(1.5); p.strokeWeight(1.5);
p.line(x, pad.top, x, p.height - pad.bottom); p.line(x, pad.top, x, p.height - pad.bottom);
// 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);
// Now, draw the circle using the same frame data
if (frameData.canVehSpeed_kmph !== null && !isNaN(frameData.canVehSpeed_kmph)) {
const canSpeed = frameData.canVehSpeed_kmph;
const y = p.map(canSpeed, minSpeed, maxSpeed, p.height - pad.bottom, pad.top);
p.fill(255, 0, 0);
p.noStroke();
p.ellipse(x, y, 8, 8);
} }
}
/**
* Handles window resizing. Resizes the canvas and recreates/redraws the static buffer.
*/
}
p.windowResized = function () { p.windowResized = function () {
p.resizeCanvas( p.resizeCanvas(
speedGraphContainer.offsetWidth, speedGraphContainer.offsetWidth,
speedGraphContainer.offsetHeight speedGraphContainer.offsetHeight
); );
// Instead of resizing the buffer, we re-create it
staticBuffer = p.createGraphics(p.width, p.height); 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);
if (appState.vizData && videoDuration) {
p.drawStaticGraphToBuffer(appState.vizData);
} }
p.redraw(); p.redraw();
}; };
};
};

6
steps/src/state.js

@ -2,10 +2,6 @@ export const appState = {
// Stores the parsed visualization data (radar frames, tracks, etc.) // Stores the parsed visualization data (radar frames, tracks, etc.)
vizData: null, vizData: null,
// Stores the processed CAN bus data (speed, time) // 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, videoStartDate: null,
// The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename // The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename
radarStartTimeMs: 0, radarStartTimeMs: 0,
@ -26,8 +22,6 @@ export const appState = {
// The filename of the loaded video file // The filename of the loaded video file
videoFilename: "", videoFilename: "",
// The filename of the loaded CAN log file // The filename of the loaded CAN log file
canLogFilename: "",
// Boolean indicating if the close-up interaction mode is active
isCloseUpMode: false, isCloseUpMode: false,
// Timestamp (from performance.now()) when the master clock started for synchronized playback // Timestamp (from performance.now()) when the master clock started for synchronized playback
masterClockStart: 0, masterClockStart: 0,

3
steps/src/sync.js

@ -5,7 +5,6 @@ import {
offsetInput, offsetInput,
stopBtn, stopBtn,
updateFrame, updateFrame,
updateCanDisplay,
updateDebugOverlay, updateDebugOverlay,
} from "./dom.js"; } from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js"; import { findRadarFrameIndexForTime } from "./utils.js";
@ -64,8 +63,6 @@ export function animationLoop() {
return; return;
} }
// Update CAN bus data display
updateCanDisplay(currentMediaTime);
// Update debug overlay information // Update debug overlay information
updateDebugOverlay(currentMediaTime); updateDebugOverlay(currentMediaTime);
// Redraw the speed graph if an instance exists // Redraw the speed graph if an instance exists

19
steps/src/theme.js

@ -22,20 +22,15 @@ function setTheme(theme) {
// Redraw the speed graph to apply theme changes // Redraw the speed graph to apply theme changes
if (appState.speedGraphInstance) { 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
// Check if there's data available to redraw
if (appState.vizData && videoPlayer.duration) {
// Re-run setData. This is the most reliable way to redraw the graph
// with the new theme, as it recalculates and redraws everything.
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
); );
} }
// Request a redraw of the speed graph to display the updated buffer
appState.speedGraphInstance.redraw();
} }
} }

21
steps/src/utils.js

@ -24,28 +24,7 @@ export function findRadarFrameIndexForTime(targetTimeMs, vizData) {
return ans; return ans;
} }
export function findLastCanIndexBefore(targetTime, canData) {
// 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 the index of the found CAN data point.
return ans;
}
export function extractTimestampInfo(filename) { export function extractTimestampInfo(filename) {
// Return null if filename is not provided // Return null if filename is not provided

Loading…
Cancel
Save