Browse Source

Track coloring basis severity (TTC). And other minor bug fixes.

refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
c04af679f7
  1. 11
      steps/index.html
  2. 3
      steps/src/dom.js
  3. 115
      steps/src/drawUtils.js
  4. 230
      steps/src/main.js
  5. 8
      steps/src/p5/radarSketch.js

11
steps/index.html

@ -148,11 +148,9 @@
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-inlier-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Inlier</label>
<!-- ADD THIS LINE -->
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-stationary-color" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Stationary</label>
<!-- END OF ADDED LINE -->
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-velocity"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked />
Show Object Details</label>
@ -171,7 +169,6 @@
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-debug2-overlay" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Show Advanced Debug</label>
<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>
@ -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
</button>
<button id="save-session-btn" class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">Save Session</button>
<button id="load-session-btn" class="bg-teal-600 text-white px-4 py-2 rounded-lg hover:bg-teal-700 transition-colors text-sm font-medium">Load Session</button>
<div class="flex items-center gap-2">
<label for="offset-input" class="text-sm font-medium">
Offset (ms):<br /><small>(+ve values if radar<br />
@ -303,9 +302,9 @@
</div>
</div>
</div>
<input type="file" id="json-file-input" class="hidden" accept=".json" /><input type="file" id="video-file-input"
class="hidden" accept="video/*" />
<input type="file" id="json-file-input" class="hidden" accept=".json" />
<input type="file" id="video-file-input"class="hidden" accept="video/*" />
<input type="file" id="session-file-input" class="hidden" accept=".json" />
<script type="module" src="./src/main.js"></script>
</body>

3
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.

115
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.
for (const track of appState.vizData.tracks) {
// Get a local instance of the TTC colors for this p5 sketch
const localTtcColors = ttcColors(p);
// --- 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.
for (const track of appState.vizData.tracks) {
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.
{ problematicTrack: track }
);
continue; // Safely skip to the next track in the loop.
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]);
} else {
// Set color and weight for moving trajectories based on theme.
p.stroke(
document.documentElement.classList.contains("dark")
? p.color(10, 170, 255, 250)
: p.color(0, 50, 255, 250)
);
p.strokeWeight(1.5);
}
// Draw the trajectory as a continuous line.
p.beginShape();
for (const pos of trajPts)
for (const pos of trajPts) {
p.vertex(pos[0] * plotScales.plotScaleX, pos[1] * plotScales.plotScaleY);
// End drawing and reset line dash.
}
p.endShape();
} else {
// --- 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 ---
}
p.drawingContext.setLineDash([]);
p.pop();
}
@ -343,7 +380,6 @@ 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)) {
@ -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]],

230
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:
@ -264,13 +408,15 @@ jsonFileInput.addEventListener("change", (event) => {
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration);
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);
}

8
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
);
}
}
}

Loading…
Cancel
Save