From c04af679f7c2225c1576315f61a1f09d7ec508ed Mon Sep 17 00:00:00 2001 From: rakadu1 Date: Fri, 5 Sep 2025 11:38:03 +0530 Subject: [PATCH] Track coloring basis severity (TTC). And other minor bug fixes. --- steps/index.html | 11 +- steps/src/dom.js | 3 + steps/src/drawUtils.js | 127 +++++++++++++------ steps/src/main.js | 244 +++++++++++++++++++++++++++++++----- steps/src/p5/radarSketch.js | 8 +- 5 files changed, 313 insertions(+), 80 deletions(-) diff --git a/steps/index.html b/steps/index.html index b26b973..cbd1063 100644 --- a/steps/index.html +++ b/steps/index.html @@ -148,11 +148,9 @@ - - @@ -171,7 +169,6 @@ - @@ -250,6 +247,8 @@ class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors text-sm font-medium"> Clear Cache + +
- - + + + diff --git a/steps/src/dom.js b/steps/src/dom.js index d395581..3d5a81f 100644 --- a/steps/src/dom.js +++ b/steps/src/dom.js @@ -58,6 +58,9 @@ export const modalProgressText = document.getElementById("modal-progress-text"); export const timelineTooltip = document.getElementById("timeline-tooltip"); export const radarInfoOverlay = document.getElementById("radar-info-overlay"); export const videoInfoOverlay = document.getElementById("video-info-overlay"); +export const saveSessionBtn = document.getElementById("save-session-btn"); +export const loadSessionBtn = document.getElementById("load-session-btn"); +export const sessionFileInput = document.getElementById("session-file-input"); //----------------------UPDATE FRAME Function----------------------// // Updates the UI to reflect the current radar frame and synchronizes video playback. diff --git a/steps/src/drawUtils.js b/steps/src/drawUtils.js index 52257cb..d1b0997 100644 --- a/steps/src/drawUtils.js +++ b/steps/src/drawUtils.js @@ -24,6 +24,17 @@ export const snrColors = (p) => ({ c5: p.color(255, 0, 0), // Red }); +// In src/drawUtils.js, add this near the other color constants + +export const ttcColors = (p) => ({ + critical: p.color(255, 0, 0), // Red for TTC <= 5s + high: p.color(255, 165, 0), // Orange for 5s < TTC <= 10s + medium: p.color(255, 255, 0), // Yellow for 10s < TTC <= 30s + low: p.color(0, 255, 0), // Green for TTC > 30s + away: p.color(0, 191, 255), // Deep Sky Blue for moving away + default: p.color(128, 128, 128), // Gray for unknown/default +}); + // Defines a palette of colors for different clusters. export const clusterColors = (p) => [ p.color(230, 25, 75), // Red @@ -253,74 +264,100 @@ export function drawPointCloud(p, points, plotScales) { * @param {p5} p - The p5 instance. * @param {object} plotScales - The calculated scales for plotting. */ +// In src/drawUtils.js, replace the entire function + export function drawTrajectories(p, plotScales) { - // Iterate through each tracked object. + // Get a local instance of the TTC colors for this p5 sketch + const localTtcColors = ttcColors(p); + for (const track of appState.vizData.tracks) { - - // --- START: Enhanced Safeguard and Detailed Logging --- - // This check is now more robust. It ensures the track object exists, - // that it has a historyLog property, and that historyLog is an array. if (!track || !track.historyLog || !Array.isArray(track.historyLog)) { - // If any check fails, print a detailed warning to the console and skip. - console.warn( - `[Visualizer Warning] Malformed track object found at frame ${appState.currentFrame + 1}. The 'historyLog' property is missing or not an array. Skipping this track.`, - { problematicTrack: track } // This logs the entire object for inspection. - ); - continue; // Safely skip to the next track in the loop. + console.warn( + `[Visualizer Warning] Malformed track object found at frame ${appState.currentFrame + 1}. The 'historyLog' property is missing or not an array. Skipping this track.`, + { problematicTrack: track } + ); + continue; } - // --- END: Enhanced Safeguard and Detailed Logging --- - // Filter history logs to include only frames up to the current one. const logs = track.historyLog.filter( (log) => log.frameIdx <= appState.currentFrame + 1 ); - // Skip if there are not enough points to draw a trajectory. if (logs.length < 2) continue; - // Get the last log entry. const lastLog = logs[logs.length - 1]; - // Skip if the trajectory is too old. if (appState.currentFrame + 1 - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH) continue; - // Adjust trajectory length based on whether the object is stationary. const isCurrentlyStationary = lastLog.isStationary; let maxLen = isCurrentlyStationary ? Math.floor(MAX_TRAJECTORY_LENGTH / 4) : MAX_TRAJECTORY_LENGTH; - // Filter and map corrected positions for the trajectory. let trajPts = logs - .filter( - (log) => log.correctedPosition && log.correctedPosition[0] !== null - ) + .filter((log) => log.correctedPosition && log.correctedPosition[0] !== null) .map((log) => log.correctedPosition); - // Slice the trajectory to the maximum allowed length. + if (trajPts.length > maxLen) { trajPts = trajPts.slice(trajPts.length - maxLen); } - // Begin drawing the trajectory. + p.push(); p.noFill(); + if (isCurrentlyStationary) { - p.stroke(34, 139, 34, 220); // Forest green + // Stationary tracks are always green and dashed + p.stroke(34, 139, 34, 220); p.strokeWeight(1); p.drawingContext.setLineDash([3, 3]); + p.beginShape(); + for (const pos of trajPts) { + p.vertex(pos[0] * plotScales.plotScaleX, pos[1] * plotScales.plotScaleY); + } + p.endShape(); } else { - // Set color and weight for moving trajectories based on theme. - p.stroke( - document.documentElement.classList.contains("dark") - ? p.color(10, 170, 255, 250) - : p.color(0, 50, 255, 250) - ); + // --- START: New TTC Coloring Logic for Moving Tracks --- + let trajectoryColor; + switch (lastLog.ttcCategory) { + case 3: + trajectoryColor = localTtcColors.critical; + break; + case 2: + trajectoryColor = localTtcColors.high; + break; + case 1: + trajectoryColor = localTtcColors.medium; + break; + case 0: + trajectoryColor = localTtcColors.low; + break; + case -1: + trajectoryColor = localTtcColors.away; + break; + default: + // Fallback to the original blue color if ttcCategory is missing + trajectoryColor = document.documentElement.classList.contains('dark') ? p.color(10, 170, 255) : p.color(0, 50, 255); + break; + } + p.strokeWeight(1.5); + p.drawingContext.setLineDash([]); // Ensure solid line for moving tracks + + // Fading trajectory logic + for (let i = 1; i < trajPts.length; i++) { + const alpha = p.map(i, 0, trajPts.length, 50, 255); + trajectoryColor.setAlpha(alpha); + p.stroke(trajectoryColor); + + const prevPt = trajPts[i - 1]; + const currPt = trajPts[i]; + p.line( + prevPt[0] * plotScales.plotScaleX, prevPt[1] * plotScales.plotScaleY, + currPt[0] * plotScales.plotScaleX, currPt[1] * plotScales.plotScaleY + ); + } + // --- END: New TTC Coloring Logic --- } - // Draw the trajectory as a continuous line. - p.beginShape(); - for (const pos of trajPts) - p.vertex(pos[0] * plotScales.plotScaleX, pos[1] * plotScales.plotScaleY); - // End drawing and reset line dash. - p.endShape(); + p.drawingContext.setLineDash([]); p.pop(); } @@ -343,13 +380,12 @@ export function drawTrackMarkers(p, plotScales) { const localMovingColor = movingColor(p); for (const track of appState.vizData.tracks) { - // --- START: Add the Same Safeguard Here --- // This robust check ensures the track and its historyLog are valid before use. if (!track || !track.historyLog || !Array.isArray(track.historyLog)) { - // We don't need to log a warning here again, as drawTrajectories already did. - // We can just safely skip this malformed track. - continue; + // We don't need to log a warning here again, as drawTrajectories already did. + // We can just safely skip this malformed track. + continue; } // --- END: Add the Same Safeguard Here --- @@ -532,7 +568,16 @@ export function handleCloseUpDisplay(p, plotScales) { } } -export function drawCovarianceEllipse(p, position, covarianceP, plotScales) { +export function drawCovarianceEllipse( + p, + position, + covarianceP, + plotScales, + isStationary +) { + // Only draw the ellipse for tracks that are not stationary. + if (isStationary) return; + const pPos = [ [covarianceP[0][0], covarianceP[0][1]], [covarianceP[1][0], covarianceP[1][1]], diff --git a/steps/src/main.js b/steps/src/main.js index dd9547d..a88924a 100644 --- a/steps/src/main.js +++ b/steps/src/main.js @@ -78,6 +78,11 @@ import { resetVisualization, updateDebugOverlay, timelineTooltip, + saveSessionBtn, + loadSessionBtn, + sessionFileInput, + togglePredictedPos, + toggleCovariance, } from "./dom.js"; import { initializeTheme } from "./theme.js"; @@ -192,6 +197,145 @@ clearCacheBtn.addEventListener("click", async () => { window.location.reload(); } }); +// Event listener for saving the session +// FILE: steps/src/main.js + +// REPLACE the existing 'saveSessionBtn' event listener with this entire block: +saveSessionBtn.addEventListener('click', () => { + // We can only save a session if at least one data file has been loaded. + if (!appState.jsonFilename && !appState.videoFilename) { + showModal("Nothing to save. Please load data files first."); + return; + } + + // Collect all relevant state into a single object. + const sessionState = { + version: 1, // For future compatibility + jsonFilename: appState.jsonFilename, + videoFilename: appState.videoFilename, + offset: offsetInput.value, + playbackSpeed: speedSlider.value, + snrMin: snrMinInput.value, + snrMax: snrMaxInput.value, + // Save the checked state of every toggle checkbox. + toggles: { + snrColor: toggleSnrColor.checked, + clusterColor: toggleClusterColor.checked, + inlierColor: toggleInlierColor.checked, + stationaryColor: toggleStationaryColor.checked, + velocity: toggleVelocity.checked, + tracks: toggleTracks.checked, + egoSpeed: toggleEgoSpeed.checked, + frameNorm: toggleFrameNorm.checked, + debugOverlay: toggleDebugOverlay.checked, + debug2Overlay: toggleDebug2Overlay.checked, + closeUp: toggleCloseUp.checked, + predictedPos: togglePredictedPos.checked, + covariance: toggleCovariance.checked, + } + }; + + const sessionString = JSON.stringify(sessionState, null, 2); + const blob = new Blob([sessionString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + // --- START: New dynamic filename logic --- + // Get the current date and time to create a timestamp. + const now = new Date(); + // Helper function to ensure numbers are two digits (e.g., 5 -> "05"). + const pad = (num) => String(num).padStart(2, '0'); + + // Format the date as YYYY-MM-DD + const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; + // Format the time as HH-mm-ss + const time = `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; + + // Combine them into a user-friendly timestamp. + const timestamp = `${date}_${time}`; + const defaultFilename = `visualizer-session_${timestamp}.json`; + // --- END: New dynamic filename logic --- + + // Create a temporary link to trigger the file download. + const a = document.createElement('a'); + a.href = url; + // Use the new dynamic filename here. The browser will open a "Save As" dialog. + a.download = defaultFilename; + document.body.appendChild(a); + a.click(); // Programmatically click the link to start the download. + + // Clean up the temporary link and URL. + document.body.removeChild(a); + URL.revokeObjectURL(url); +}); + +// When "Load Session" is clicked, it triggers the hidden file input. +loadSessionBtn.addEventListener("click", () => { + sessionFileInput.click(); +}); + +// This listener handles the selected session file. + +sessionFileInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (e) => { // Make the function async to use 'await' + try { + const sessionState = JSON.parse(e.target.result); + + // Basic validation to ensure it's a valid session file. + if (sessionState.version !== 1 || !sessionState.jsonFilename) { + showModal("Error: Invalid or corrupted session file."); + return; + } + + // --- START: New Robust Session Check --- + + // 1. Before doing anything else, check if the required files exist in the cache. + // We use the same 'loadFreshFileFromDB' function that the startup process uses. + const videoBlob = await loadFreshFileFromDB("video", sessionState.videoFilename); + const jsonBlob = await loadFreshFileFromDB("json", sessionState.jsonFilename); + + // 2. If either file is missing from the cache, show an informative error and stop. + if (!jsonBlob || !videoBlob) { + showModal(`Session load failed: The required data files are not in the application's cache. + + Please manually load '${sessionState.jsonFilename}' and '${sessionState.videoFilename}' before loading this session.`); + + event.target.value = ''; // Reset file input + return; + } + + // 3. If we get here, it means the files ARE in the cache and match the session! + // It is now safe to set localStorage and reload the page. + + localStorage.setItem('jsonFilename', sessionState.jsonFilename || ''); + localStorage.setItem('videoFilename', sessionState.videoFilename || ''); + localStorage.setItem('visualizerOffset', sessionState.offset || '0'); + localStorage.setItem('playbackSpeed', sessionState.playbackSpeed || '1'); + localStorage.setItem('snrMin', sessionState.snrMin || ''); + localStorage.setItem('snrMax', sessionState.snrMax || ''); + if (sessionState.toggles) { + localStorage.setItem('togglesState', JSON.stringify(sessionState.toggles)); + } + + // Inform the user and then reload the page to apply the session. + showModal("Session files found in cache. The application will now reload.").then(() => { + window.location.reload(); + }); + // --- END: New Robust Session Check --- + + } catch (error) { + showModal("Error: Could not parse the session file. It may be invalid."); + console.error("Session load error:", error); + } + }; + reader.readAsText(file); + event.target.value = ''; // Clear the input for future loads. +}); + +// --- END: Add Session Management Logic --- // In main.js, REPLACE your existing jsonFileInput event listener with this entire block: @@ -231,7 +375,7 @@ jsonFileInput.addEventListener("change", (event) => { worker.terminate(); // Terminate worker on error return; } - + if (appState.p5_instance) { appState.p5_instance.remove(); appState.p5_instance = null; @@ -241,7 +385,7 @@ jsonFileInput.addEventListener("change", (event) => { appState.speedGraphInstance = null; speedGraphPlaceholder.classList.remove("hidden"); } - + appState.vizData = result.data; appState.globalMinSnr = result.minSnr; appState.globalMaxSnr = result.maxSnr; @@ -255,22 +399,24 @@ jsonFileInput.addEventListener("change", (event) => { if (!appState.p5_instance) { appState.p5_instance = new p5(radarSketch); } - + // --- START: This is the new, corrected logic --- // After processing the new JSON, check if a video is already loaded and ready. // If it is, this is the trigger to create or update the speed graph. if (appState.vizData && videoPlayer.duration > 0) { - speedGraphPlaceholder.classList.add("hidden"); - if (!appState.speedGraphInstance) { - appState.speedGraphInstance = new p5(speedGraphSketch); - } - appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration); + speedGraphPlaceholder.classList.add("hidden"); + if (!appState.speedGraphInstance) { + appState.speedGraphInstance = new p5(speedGraphSketch); + } + appState.speedGraphInstance.setData( + appState.vizData, + videoPlayer.duration + ); } // --- END: This is the new, corrected logic --- document.getElementById("modal-ok-btn").click(); worker.terminate(); - } else if (type === "error") { showModal(message); worker.terminate(); @@ -608,30 +754,72 @@ function calculateAndSetOffset() { } // Application Initialization -// In src/main.js, REPLACE the entire 'DOMContentLoaded' listener with this: -// In src/main.js, replace the existing DOMContentLoaded listener with this entire block: +// FILE: steps/src/main.js -// In src/main.js, replace the existing DOMContentLoaded listener with this entire block: +// REPLACE the entire 'document.addEventListener("DOMContentLoaded", ...)' block with this: document.addEventListener("DOMContentLoaded", () => { initializeTheme(); console.log("DEBUG: DOMContentLoaded fired. Starting session load."); initDB(async () => { - // Make the callback async to use await console.log("DEBUG: Database initialized."); + + // --- START: Restore Session and UI State from localStorage --- const savedOffset = localStorage.getItem("visualizerOffset"); if (savedOffset !== null) { offsetInput.value = savedOffset; } + const savedSpeed = localStorage.getItem("playbackSpeed"); + if (savedSpeed) { + speedSlider.value = savedSpeed; + speedDisplay.textContent = `${parseFloat(savedSpeed).toFixed(1)}x`; + videoPlayer.playbackRate = savedSpeed; + } + + const savedSnrMin = localStorage.getItem("snrMin"); + if (savedSnrMin) snrMinInput.value = savedSnrMin; + + const savedSnrMax = localStorage.getItem("snrMax"); + if (savedSnrMax) snrMaxInput.value = savedSnrMax; + + // If custom SNR values were part of the session, apply them to the app state. + if (savedSnrMin && savedSnrMax) { + appState.globalMinSnr = parseFloat(savedSnrMin); + appState.globalMaxSnr = parseFloat(savedSnrMax); + } + + // Restore the state of all toggle checkboxes. + const savedToggles = localStorage.getItem("togglesState"); + if (savedToggles) { + try { + const toggles = JSON.parse(savedToggles); + toggleSnrColor.checked = toggles.snrColor; + toggleClusterColor.checked = toggles.clusterColor; + toggleInlierColor.checked = toggles.inlierColor; + toggleStationaryColor.checked = toggles.stationaryColor; + toggleVelocity.checked = toggles.velocity; + toggleTracks.checked = toggles.tracks; + toggleEgoSpeed.checked = toggles.egoSpeed; + toggleFrameNorm.checked = toggles.frameNorm; + toggleDebugOverlay.checked = toggles.debugOverlay; + toggleDebug2Overlay.checked = toggles.debug2Overlay; + toggleCloseUp.checked = toggles.closeUp; + togglePredictedPos.checked = toggles.predictedPos; + toggleCovariance.checked = toggles.covariance; + } catch (e) { + console.error("Could not parse saved toggle state.", e); + } + } + // --- END: Restore Session and UI State --- + // Get the filenames we EXPECT to load from localStorage appState.videoFilename = localStorage.getItem("videoFilename"); appState.jsonFilename = localStorage.getItem("jsonFilename"); calculateAndSetOffset(); - // Asynchronously load files, performing freshness and integrity checks const videoBlob = await loadFreshFileFromDB( "video", appState.videoFilename @@ -642,7 +830,6 @@ document.addEventListener("DOMContentLoaded", () => { "DEBUG: Freshness checks complete. Proceeding with valid data." ); - // This function processes the parsed JSON and sets up the main visualization state const finalizeSetup = async (parsedJson) => { if (parsedJson) { const result = await parseVisualizationJson( @@ -653,16 +840,20 @@ document.addEventListener("DOMContentLoaded", () => { if (!result.error) { appState.vizData = result.data; - appState.globalMinSnr = result.minSnr; - appState.globalMaxSnr = result.maxSnr; - snrMinInput.value = result.minSnr.toFixed(1); - snrMaxInput.value = result.maxSnr.toFixed(1); + // Note: We use the saved SNR values if they exist, otherwise the file's global values. + appState.globalMinSnr = savedSnrMin + ? parseFloat(savedSnrMin) + : result.minSnr; + appState.globalMaxSnr = savedSnrMax + ? parseFloat(savedSnrMax) + : result.maxSnr; + snrMinInput.value = savedSnrMin || result.minSnr.toFixed(1); + snrMaxInput.value = savedSnrMax || result.maxSnr.toFixed(1); } else { showModal(result.error); } } - // Final UI updates for the radar canvas if (appState.vizData) { resetVisualization(); canvasPlaceholder.style.display = "none"; @@ -673,38 +864,27 @@ document.addEventListener("DOMContentLoaded", () => { } }; - // --- Main Loading Logic --- if (jsonBlob) { - // CASE 1: Cached JSON exists. Parse it first with a progress bar. showModal("Loading data from cache...", false, true); updateModalProgress(0); - const worker = new Worker("./src/parser.worker.js"); - worker.onmessage = async (e) => { const { type, data, message, percent } = e.data; - if (type === "progress") { updateModalProgress(percent); } else if (type === "complete") { updateModalProgress(100); - await finalizeSetup(data); // Process the parsed JSON - - // Hide the JSON loading modal before starting the video load + await finalizeSetup(data); document.getElementById("modal-ok-btn").click(); worker.terminate(); - - // Now that JSON is ready, load the video (which will show its own modal) loadVideoWithProgress(videoBlob); } else if (type === "error") { showModal(message); worker.terminate(); } }; - worker.postMessage({ file: jsonBlob }); } else { - // CASE 2: No cached JSON. Finalize setup with null data and just load the video if it exists. await finalizeSetup(null); loadVideoWithProgress(videoBlob); } diff --git a/steps/src/p5/radarSketch.js b/steps/src/p5/radarSketch.js index 8cda624..979d792 100644 --- a/steps/src/p5/radarSketch.js +++ b/steps/src/p5/radarSketch.js @@ -105,7 +105,13 @@ export const radarSketch = function (p) { if (log && log.covarianceP) { const pos = log.predictedPosition; if (pos && pos[0] !== null) { - drawCovarianceEllipse(p, pos, log.covarianceP, plotScales); + drawCovarianceEllipse( + p, + pos, + log.covarianceP, + plotScales, + log.isStationary + ); } } }