Browse Source

Debounce for timeline slider. Scrub to timeline slider. etc.

refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
3f89457706
  1. 95
      steps/Improvements.txt
  2. 4
      steps/index.html
  3. 2
      steps/src/dom.js
  4. 163
      steps/src/main.js
  5. 3
      steps/src/state.js
  6. 17
      steps/src/utils.js

95
steps/Improvements.txt

@ -0,0 +1,95 @@
Improvements:-
--------------------------------------------------------------------------------------------------------------------------------------------
1.) Data Analysis & Interaction
a)Object Selection & Persistent Info Panel:
Idea: Allow the user to click on a track marker (+). This would "select" the object, highlighting its trajectory permanently. A new sidebar panel would appear, displaying the selected object's detailed data (ID, current speed, TTC, position, etc.) in real-time as the visualization plays.
Benefit: Makes it much easier to analyze the behavior of a single, specific object throughout the entire log without needing to hover over it.
b)Measurement Tool:
Idea: Add a "ruler" mode. The user could click two points on the radar canvas (e.g., the ego vehicle and a track, or two different tracks) to see a line drawn between them with the real-world distance in meters displayed.
Benefit: Allows for quick and easy spatial analysis, like checking the distance between vehicles in a critical scenario.
c)Advanced Data Filtering:
Idea: Add a small "Filter" panel with inputs that allow you to dynamically filter what's shown on screen. For example:
"Only show points with SNR > 15"
"Only show tracks with Speed > 50 km/h"
"Only show tracks with TTC < 2.0 seconds"
Benefit: Helps to de-clutter the visualization and focus only on the data that is relevant to a specific investigation.
--------------------------------------------------------------------------------------------------------------------------------------------
2.) Visualization Enhancements
a) Ego Vehicle Representation:
Idea: Draw a simple car icon or a shaded rectangle at the center-bottom of the radar plot to represent the "ego vehicle" (your car).
Benefit: Provides an immediate and clear visual anchor, making the spatial relationship between your vehicle and the surrounding tracks much more intuitive.
b) Fading Trajectories:
Idea: Modify the track drawing logic so that older points in an object's trajectory are drawn with progressively higher transparency (fading out).
Benefit: This creates a "comet tail" effect that gives a much better intuitive sense of the object's recent direction of travel.
c)Bounding Boxes for Tracks:
Idea: If your tracking data ever includes object dimensions (length and width), we could replace the simple "+" marker with a 2D rectangle (a bounding box) that represents the object's size.
Benefit: Provides a much more realistic representation of the scene and the space occupied by other vehicles.
--------------------------------------------------------------------------------------------------------------------------------------------
3.)User Experience (UX) & Workflow
a)Session Management ("Projects"):
Idea: Add "Save Session" and "Load Session" buttons. A session file (.json) would save the names of the loaded files, the manually tuned time offset, and the state of all the UI toggles.
Benefit: Allows you to instantly return to a specific analysis setup without having to reload all files and re-configure the UI every time.
b)More Keyboard Shortcuts:
Idea: Implement more shortcuts for power users.
Spacebar for Play/Pause.
M to mute/unmute the video.
Number keys (1, 2, 3...) to quickly switch between the coloring modes (SNR, Cluster, etc.).
Benefit: Speeds up the workflow significantly for frequent users.
c)Resizable Layout:
Idea: Implement a draggable vertical divider between the radar canvas and the video panel, allowing the user to resize them to focus on one or the other.
Benefit: Provides flexibility for different analysis tasks. Sometimes you want a bigger video, other times a bigger radar plot.
--------------------------------------------------------------------------------------------------------------------------------------------
4.)Data Export & Integration
a)Save Canvas Snapshot:
Idea: A simple "Camera" button that saves the current view of the radar canvas as a PNG image file.
Benefit: Perfect for quickly capturing interesting moments for reports, presentations, or bug tracking.

4
steps/index.html

@ -217,6 +217,10 @@
<footer class="bg-white dark:bg-gray-800 shadow-up w-full p-4 mt-auto sticky bottom-0 z-20"> <footer class="bg-white dark:bg-gray-800 shadow-up w-full p-4 mt-auto sticky bottom-0 z-20">
<div class="mb-4"> <div class="mb-4">
<div id="timeline-tooltip"
class="absolute hidden bg-gray-900 dark:bg-gray-700 text-white text-xs rounded py-1 px-2 pointer-events-none text-center"
style="transform: translate(-50%, -125%);">
</div>
<input type="range" id="timeline-slider" min="0" max="0" value="0" <input type="range" id="timeline-slider" min="0" max="0" value="0"
class="w-full h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer" /> class="w-full h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer" />
</div> </div>

2
steps/src/dom.js

@ -50,10 +50,10 @@ 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");
// In src/dom.js, add these exports
export const modalProgressContainer = document.getElementById("modal-progress-container"); export const modalProgressContainer = document.getElementById("modal-progress-container");
export const modalProgressBar = document.getElementById("modal-progress-bar"); export const modalProgressBar = document.getElementById("modal-progress-bar");
export const modalProgressText = document.getElementById("modal-progress-text"); export const modalProgressText = document.getElementById("modal-progress-text");
export const timelineTooltip = document.getElementById("timeline-tooltip");
//----------------------UPDATE FRAME Function----------------------// //----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback. // Updates the UI to reflect the current radar frame and synchronizes video playback.

163
steps/src/main.js

@ -34,6 +34,7 @@ import {
extractTimestampInfo, extractTimestampInfo,
parseTimestamp, parseTimestamp,
throttle, throttle,
formatTime,
} from "./utils.js"; } from "./utils.js";
import { appState } from "./state.js"; import { appState } from "./state.js";
import { import {
@ -76,12 +77,15 @@ import {
updateFrame, updateFrame,
resetVisualization, resetVisualization,
updateDebugOverlay, updateDebugOverlay,
timelineTooltip,
} from "./dom.js"; } from "./dom.js";
import { initializeTheme } from "./theme.js"; import { initializeTheme } from "./theme.js";
import { initDB, saveFileWithMetadata, loadFreshFileFromDB } from "./db.js"; import { initDB, saveFileWithMetadata, loadFreshFileFromDB } from "./db.js";
let seekDebounceTimer = null; // Add this line
// Sets up the video player with the given file URL. // Sets up the video player with the given file URL.
function setupVideoPlayer(fileURL) { function setupVideoPlayer(fileURL) {
videoPlayer.src = fileURL; videoPlayer.src = fileURL;
@ -130,13 +134,17 @@ function loadVideoWithProgress(videoObject) {
// This one-time event is for re-syncing data once the video's metadata is ready // This one-time event is for re-syncing data once the video's metadata is ready
videoPlayer.addEventListener('loadedmetadata', () => {
videoPlayer.addEventListener(
"loadedmetadata",
() => {
// This is the perfect time to re-sync data if needed // This is the perfect time to re-sync data if needed
if (appState.vizData) { if (appState.vizData) {
console.log("DEBUG: Video metadata loaded. Re-calculating timestamps."); console.log("DEBUG: Video metadata loaded. Re-calculating timestamps.");
appState.vizData.radarFrames.forEach((frame) => { appState.vizData.radarFrames.forEach((frame) => {
frame.timestampMs = appState.radarStartTimeMs + frame.timestamp - appState.videoStartDate.getTime();
frame.timestampMs =
appState.radarStartTimeMs +
frame.timestamp -
appState.videoStartDate.getTime();
}); });
resetVisualization(); resetVisualization();
} }
@ -148,11 +156,15 @@ videoPlayer.addEventListener('loadedmetadata', () => {
if (!appState.speedGraphInstance) { if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch); appState.speedGraphInstance = new p5(speedGraphSketch);
} }
appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration);
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
} }
// --- END: New Speed Graph Logic --- // --- END: New Speed Graph Logic ---
}, { once: true }); // { once: true } makes sure this runs only once per load
},
{ once: true }
); // { once: true } makes sure this runs only once per load
// { once: true } //makes sure this runs only once per load // { once: true } //makes sure this runs only once per load
@ -339,10 +351,20 @@ stopBtn.addEventListener("click", () => {
}); });
// Event listener for timeline slider input. // Event listener for timeline slider input.
timelineSlider.addEventListener(
"input",
throttle((event) => {
// In src/main.js, REPLACE the existing timelineSlider 'input' event listener with this:
timelineSlider.addEventListener("input", (event) => {
if (!appState.vizData) return; if (!appState.vizData) return;
// --- 1. Live Seeking (Throttled for performance) ---
// This part gives you the immediate visual feedback as you drag the slider.
// We use a simple timestamp check to prevent it from running too often.
const now = performance.now();
if (
!timelineSlider.lastInputTime ||
now - timelineSlider.lastInputTime > 32
) {
// ~30fps throttle
if (appState.isPlaying) { if (appState.isPlaying) {
videoPlayer.pause(); videoPlayer.pause();
appState.isPlaying = false; appState.isPlaying = false;
@ -351,9 +373,64 @@ timelineSlider.addEventListener(
const frame = parseInt(event.target.value, 10); const frame = parseInt(event.target.value, 10);
updateFrame(frame, true); updateFrame(frame, true);
appState.mediaTimeStart = videoPlayer.currentTime; appState.mediaTimeStart = videoPlayer.currentTime;
appState.masterClockStart = performance.now();
}, 16)
);
appState.masterClockStart = now;
timelineSlider.lastInputTime = now;
}
// --- 2. Final, Precise Sync (Debounced for reliability) ---
// This part ensures a perfect sync only AFTER you stop moving the slider.
clearTimeout(seekDebounceTimer); // Always cancel the previously scheduled sync
seekDebounceTimer = setTimeout(() => {
console.log("Slider movement stopped. Performing final, debounced resync.");
const finalFrame = parseInt(event.target.value, 10);
updateFrame(finalFrame, true); // Perform the final, precise seek
// Also update the debug overlay with the final, settled time
updateDebugOverlay(videoPlayer.currentTime);
}, 250); // Wait for 250ms of inactivity before firing
});
// In src/main.js, add this new block of event listeners
// --- Timeline Scrub-to-Seek Preview Logic ---
timelineSlider.addEventListener("mouseover", () => {
if (appState.vizData) {
timelineTooltip.classList.remove("hidden");
}
});
timelineSlider.addEventListener("mouseout", () => {
timelineTooltip.classList.add("hidden");
});
timelineSlider.addEventListener("mousemove", (event) => {
if (!appState.vizData) return;
// 1. Calculate the hover position as a fraction (0.0 to 1.0)
const rect = timelineSlider.getBoundingClientRect();
const hoverFraction = (event.clientX - rect.left) / rect.width;
// 2. Calculate the corresponding frame index
const sliderMax = parseInt(timelineSlider.max, 10) || (appState.vizData.radarFrames.length - 1);
let frameIndex = Math.round(hoverFraction * sliderMax);
// The value is already clamped by this calculation, but an extra check is safe
frameIndex = Math.max(0, Math.min(frameIndex, sliderMax));
const frameData = appState.vizData.radarFrames[frameIndex];
if (!frameData) return;
// 3. Update the tooltip's content
const formattedTime = formatTime(frameData.timestampMs);
timelineTooltip.innerHTML = `Frame: ${
frameIndex + 1
}<br>Time: ${formattedTime}`;
// 4. Position the tooltip horizontally above the cursor
// The horizontal position is the mouse's X relative to the slider's start
const tooltipX = event.clientX - rect.left;
timelineTooltip.style.left = `${tooltipX}px`;
});
// Event listener for speed slider input. // Event listener for speed slider input.
speedSlider.addEventListener("input", (event) => { speedSlider.addEventListener("input", (event) => {
@ -449,22 +526,6 @@ document.addEventListener("keydown", (event) => {
} }
}); });
// In src/main.js, add this new event listener
videoPlayer.addEventListener("seeked", () => {
// This event fires every time a seek operation completes.
// We only act if our flag has been set.
if (appState.needsPostSeekUpdate) {
console.log(
"Video has finished seeking. Performing final debug overlay update."
);
// Now we can be sure videoPlayer.currentTime is accurate.
updateDebugOverlay(videoPlayer.currentTime);
// Reset the flag so this logic doesn't run on every seek
appState.needsPostSeekUpdate = false;
}
});
function calculateAndSetOffset() { function calculateAndSetOffset() {
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
@ -508,7 +569,8 @@ document.addEventListener("DOMContentLoaded", () => {
initializeTheme(); initializeTheme();
console.log("DEBUG: DOMContentLoaded fired. Starting session load."); console.log("DEBUG: DOMContentLoaded fired. Starting session load.");
initDB(async () => { // Make the callback async to use await
initDB(async () => {
// Make the callback async to use await
console.log("DEBUG: Database initialized."); console.log("DEBUG: Database initialized.");
const savedOffset = localStorage.getItem("visualizerOffset"); const savedOffset = localStorage.getItem("visualizerOffset");
if (savedOffset !== null) { if (savedOffset !== null) {
@ -522,10 +584,15 @@ document.addEventListener("DOMContentLoaded", () => {
calculateAndSetOffset(); calculateAndSetOffset();
// Asynchronously load files, performing freshness and integrity checks // Asynchronously load files, performing freshness and integrity checks
const videoBlob = await loadFreshFileFromDB("video", appState.videoFilename);
const videoBlob = await loadFreshFileFromDB(
"video",
appState.videoFilename
);
const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename); const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename);
console.log("DEBUG: Freshness checks complete. Proceeding with valid data.");
console.log(
"DEBUG: Freshness checks complete. Proceeding with valid data."
);
// This function processes the parsed JSON and sets up the main visualization state // This function processes the parsed JSON and sets up the main visualization state
const finalizeSetup = async (parsedJson) => { const finalizeSetup = async (parsedJson) => {
@ -564,14 +631,14 @@ document.addEventListener("DOMContentLoaded", () => {
showModal("Loading data from cache...", false, true); showModal("Loading data from cache...", false, true);
updateModalProgress(0); updateModalProgress(0);
const worker = new Worker('./src/parser.worker.js');
const worker = new Worker("./src/parser.worker.js");
worker.onmessage = async (e) => { worker.onmessage = async (e) => {
const { type, data, message, percent } = e.data; const { type, data, message, percent } = e.data;
if (type === 'progress') {
if (type === "progress") {
updateModalProgress(percent); updateModalProgress(percent);
} else if (type === 'complete') {
} else if (type === "complete") {
updateModalProgress(100); updateModalProgress(100);
await finalizeSetup(data); // Process the parsed JSON await finalizeSetup(data); // Process the parsed JSON
@ -581,14 +648,13 @@ document.addEventListener("DOMContentLoaded", () => {
// Now that JSON is ready, load the video (which will show its own modal) // Now that JSON is ready, load the video (which will show its own modal)
loadVideoWithProgress(videoBlob); loadVideoWithProgress(videoBlob);
} else if (type === 'error') {
} else if (type === "error") {
showModal(message); showModal(message);
worker.terminate(); worker.terminate();
} }
}; };
worker.postMessage({ file: jsonBlob }); worker.postMessage({ file: jsonBlob });
} else { } else {
// CASE 2: No cached JSON. Finalize setup with null data and just load the video if it exists. // CASE 2: No cached JSON. Finalize setup with null data and just load the video if it exists.
await finalizeSetup(null); await finalizeSetup(null);
@ -621,28 +687,3 @@ offsetInput.addEventListener("keydown", (event) => {
updateFrame(appState.currentFrame, true); updateFrame(appState.currentFrame, true);
} }
}); });
// In src/main.js, REPLACE the 'change' event listener with this:
timelineSlider.addEventListener("change", () => {
if (!appState.vizData || appState.isPlaying) return;
const currentRadarFrame = appState.vizData.radarFrames[appState.currentFrame];
if (!currentRadarFrame) return;
const targetRadarTimeMs = currentRadarFrame.timestampMs;
const offsetMs = parseFloat(offsetInput.value) || 0;
const currentVideoTimeMs = videoPlayer.currentTime * 1000;
const driftMs = currentVideoTimeMs + offsetMs - targetRadarTimeMs;
if (Math.abs(driftMs) > 50) {
console.log(
`Setting flag for post-seek update. Initial drift: ${driftMs.toFixed(
0
)}ms`
);
// 1. Set the flag to true
appState.needsPostSeekUpdate = true;
// 2. Initiate the final seek operation
updateFrame(appState.currentFrame, true);
}
});

3
steps/src/state.js

@ -29,6 +29,5 @@ export const appState = {
mediaTimeStart: 0, mediaTimeStart: 0,
// Timestamp (from performance.now()) of the last synchronization check // Timestamp (from performance.now()) of the last synchronization check
lastSyncTime: 0, lastSyncTime: 0,
// new flag for seek finished
needsPostSeekUpdate: false,
}; };

17
steps/src/utils.js

@ -129,3 +129,20 @@ export function throttle(func, delay) {
return func(...args); // Apply the original function with its arguments. return func(...args); // Apply the original function with its arguments.
}; };
} }
/**
* Formats milliseconds into a MM:SS.ms string.
* @param {number} milliseconds The time in milliseconds.
* @returns {string} The formatted time string.
*/
export function formatTime(milliseconds) {
if (isNaN(milliseconds) || milliseconds < 0) {
return "00:00.000";
}
const totalSeconds = milliseconds / 1000;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.floor(totalSeconds % 60);
const ms = Math.round(milliseconds % 1000);
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(ms).padStart(3, '0')}`;
}
Loading…
Cancel
Save