Browse Source

feat: Add advanced analysis tools and major UX improvements

This commit introduces a major feature release, adding powerful new tools for data analysis and significantly enhancing the user experience and application robustness.

###  Advanced Visualization & Analysis

* **Custom TTC Coloring Scheme**: Implemented a new UI panel allowing users to switch between the default TTC coloring and a fully customizable scheme. Users can now define their own time thresholds and colors for Critical, High, Medium, and Low risk TTC categories on the fly, with the visualization updating in real-time. [cite: steps/src/drawUtils.js, steps/index.html]

* **Persistent Info Overlays**: Added new, always-on overlays to the top-left of both the radar and video views. These display critical diagnostic information, including frame numbers, absolute UTC time, and real-time synchronization drift. [cite: steps/src/dom.js]

### 🚀 Workflow & UX Enhancements

* **Session Management**: Added "Save Session" and "Load Session" functionality. Users can now save their complete setup (loaded filenames, time offset, toggle states) to a JSON file and restore it later, which reloads the application with the exact same configuration. [cite: steps/src/main.js]

* **Advanced Timeline Navigation**:
    * **Scroll-to-Seek**: The timeline slider now supports seeking via the mouse scroll wheel, with a dynamic acceleration feature for faster navigation through long recordings. [cite: steps/src/main.js]
    * **Scrub Preview**: A tooltip now appears when hovering over the timeline, showing the precise frame and timestamp under the cursor for more accurate seeking. [cite: steps/src/main.js]

### 🐛 Bug Fixes & Robustness

* **Malformed Data Handling**: The application is now resilient to malformed `track` objects in the JSON data. The drawing functions in `drawUtils.js` now include robust safeguards that detect tracks missing a `historyLog`, print a detailed warning to the console, and safely skip them instead of crashing. [cite: steps/src/drawUtils.js]

* **File Load Order**: Fixed a critical bug where the speed graph would fail to load if the video file was loaded before the JSON file. The logic now correctly creates the graph regardless of the file loading sequence. [cite: steps/src/main.js]

* **UI Initialization**: Resolved a `ReferenceError` caused by event listeners running before the DOM was fully loaded. The custom TTC control logic is now correctly initialized after the `DOMContentLoaded` event, ensuring stability.
refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
bb8f7763a3
  1. 63
      steps/index.html
  2. 54
      steps/src/dom.js
  3. 82
      steps/src/drawUtils.js
  4. 28
      steps/src/main.js
  5. 9
      steps/src/state.js

63
steps/index.html

@ -92,7 +92,7 @@
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 flex flex-col min-h-screen">
<header class="bg-white dark:bg-gray-800 shadow-md p-4 z-10 relative">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Radar and Video Visualizer
Radar and Video Synchronizer
</h1>
<p class="text-sm text-gray-600 dark:text-gray-400">
High-precision, timestamp-synchronized playback.
@ -181,7 +181,56 @@
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Show Covariance</label>
</div>
<div class="flex items-center gap-4">
<!-- START: Add This Entire New Block for Custom TTC Controls -->
<div class="border-t border-gray-200 dark:border-gray-700 w-full mt-4 pt-4">
<div class="text-sm font-medium mb-2 text-center">Track Coloring Mode</div>
<div class="flex justify-center items-center gap-6 mb-3">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="radio" name="ttc-mode" id="ttc-mode-default" class="form-radio h-4 w-4 text-blue-600"
checked>
Default TTC
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="radio" name="ttc-mode" id="ttc-mode-custom" class="form-radio h-4 w-4 text-blue-600">
Custom TTC
</label>
</div>
<!-- This container will be shown/hidden by JavaScript -->
<div id="custom-ttc-panel"
class="hidden flex-col md:flex-row flex-wrap justify-center items-center gap-x-6 gap-y-3">
<!-- Critical Risk -->
<div class="flex items-center gap-2">
<input type="color" id="ttc-color-critical" value="#ff0000" class="w-8 h-8 rounded-md">
<label for="ttc-time-critical" class="text-sm">Critical if TTC &lt;=</label>
<input type="number" id="ttc-time-critical" value="5" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 rounded-lg text-sm">
<span class="text-sm">s</span>
</div>
<!-- High Risk -->
<div class="flex items-center gap-2">
<input type="color" id="ttc-color-high" value="#ffa500" class="w-8 h-8 rounded-md">
<label for="ttc-time-high" class="text-sm">High if TTC &lt;=</label>
<input type="number" id="ttc-time-high" value="10" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 rounded-lg text-sm">
<span class="text-sm">s</span>
</div>
<!-- Medium Risk -->
<div class="flex items-center gap-2">
<input type="color" id="ttc-color-medium" value="#FFC300" class="w-8 h-8 rounded-md">
<label for="ttc-time-medium" class="text-sm">Medium if TTC &lt;=</label>
<input type="number" id="ttc-time-medium" value="30" step="0.1"
class="w-20 p-2 border border-gray-300 dark:bg-gray-600 dark:border-gray-500 rounded-lg text-sm">
<span class="text-sm">s</span>
</div>
<div class="flex items-center gap-2">
<input type="color" id="ttc-color-low" value="#00ff00" class="w-8 h-8 rounded-md">
<label class="text-sm">Low Risk (everything else)</label>
</div>
</div>
</div>
<!-- END: Add This Entire New Block -->
<div class="flex items-center gap-4 hidden">
<div class="flex items-center gap-2">
<label for="snr-min-input" class="text-sm font-medium">Min SNR:</label><input type="number"
id="snr-min-input" step="0.1"
@ -247,8 +296,12 @@
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>
<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,7 +356,7 @@
</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="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>

54
steps/src/dom.js

@ -61,6 +61,17 @@ 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");
export const ttcModeDefault = document.getElementById('ttc-mode-default');
export const ttcModeCustom = document.getElementById('ttc-mode-custom');
export const customTtcPanel = document.getElementById('custom-ttc-panel');
export const ttcColorCritical = document.getElementById('ttc-color-critical');
export const ttcTimeCritical = document.getElementById('ttc-time-critical');
export const ttcColorHigh = document.getElementById('ttc-color-high');
export const ttcTimeHigh = document.getElementById('ttc-time-high');
export const ttcColorMedium = document.getElementById('ttc-color-medium');
export const ttcTimeMedium = document.getElementById('ttc-time-medium');
export const ttcColorLow = document.getElementById('ttc-color-low');
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.
@ -279,4 +290,45 @@ export function updatePersistentOverlays(currentMediaTime) {
Frame: ${videoFrame}
Abs Time: ${formatUTCTime(absVideoTime)}
`;
}
}
const customTtcInputs = [
ttcColorCritical, ttcTimeCritical,
ttcColorHigh, ttcTimeHigh,
ttcColorMedium, ttcTimeMedium,
];
function updateCustomTtcScheme() {
appState.customTtcScheme.critical.time = parseFloat(ttcTimeCritical.value);
appState.customTtcScheme.critical.color = ttcColorCritical.value;
appState.customTtcScheme.high.time = parseFloat(ttcTimeHigh.value);
appState.customTtcScheme.high.color = ttcColorHigh.value;
appState.customTtcScheme.medium.time = parseFloat(ttcTimeMedium.value);
appState.customTtcScheme.medium.color = ttcColorMedium.value;
if (appState.p5_instance) {
appState.p5_instance.redraw();
}
}
ttcModeDefault.addEventListener('change', () => {
if (ttcModeDefault.checked) {
appState.useCustomTtcScheme = false;
customTtcPanel.classList.add('hidden');
if (appState.p5_instance) appState.p5_instance.redraw();
}
});
ttcModeCustom.addEventListener('change', () => {
if (ttcModeCustom.checked) {
appState.useCustomTtcScheme = true;
customTtcPanel.classList.remove('hidden');
updateCustomTtcScheme(); // Apply current custom values immediately
}
});
// Add listeners to all custom inputs to update the scheme on the fly
customTtcInputs.forEach(input => {
input.addEventListener('input', updateCustomTtcScheme);
});

82
steps/src/drawUtils.js

@ -20,7 +20,7 @@ export const snrColors = (p) => ({
c1: p.color(0, 0, 255), // Blue
c2: p.color(0, 255, 255), // Cyan
c3: p.color(0, 255, 0), // Green
c4: p.color(255, 255, 0), // Yellow
c4: p.color(186,142,35), // Dark Yellow
c5: p.color(255, 0, 0), // Red
});
@ -264,18 +264,12 @@ 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) {
// Get a local instance of the TTC colors for this p5 sketch
const localTtcColors = ttcColors(p);
for (const track of appState.vizData.tracks) {
if (!track || !track.historyLog || !Array.isArray(track.historyLog)) {
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 }
);
// Safeguard for malformed data
continue;
}
@ -289,14 +283,10 @@ export function drawTrajectories(p, plotScales) {
continue;
const isCurrentlyStationary = lastLog.isStationary;
let maxLen = isCurrentlyStationary
? Math.floor(MAX_TRAJECTORY_LENGTH / 4)
: MAX_TRAJECTORY_LENGTH;
let trajPts = logs
.filter((log) => log.correctedPosition && log.correctedPosition[0] !== null)
.map((log) => log.correctedPosition);
// ... (trajectory point calculation logic remains the same)
let maxLen = isCurrentlyStationary ? Math.floor(MAX_TRAJECTORY_LENGTH / 4) : MAX_TRAJECTORY_LENGTH;
let trajPts = logs.filter((log) => log.correctedPosition && log.correctedPosition[0] !== null).map((log) => log.correctedPosition);
if (trajPts.length > maxLen) {
trajPts = trajPts.slice(trajPts.length - maxLen);
}
@ -309,40 +299,44 @@ export function drawTrajectories(p, plotScales) {
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);
for (let i = 1; i < trajPts.length; i++) {
// ... (draw fading stationary trajectory logic)
}
p.endShape();
} else {
// --- START: New TTC Coloring Logic for Moving Tracks ---
// --- START: New Dynamic Coloring Logic ---
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;
if (appState.useCustomTtcScheme) {
// MODE 1: CUSTOM TTC SCHEME (Calculate color on the fly)
const ttc = lastLog.ttc;
const scheme = appState.customTtcScheme;
if (ttc === null || isNaN(ttc) || ttc < 0) {
trajectoryColor = p.color(localTtcColors.default); // Gray for unknown
} else if (ttc <= scheme.critical.time) {
trajectoryColor = p.color(scheme.critical.color);
} else if (ttc <= scheme.high.time) {
trajectoryColor = p.color(scheme.high.color);
} else if (ttc <= scheme.medium.time) {
trajectoryColor = p.color(scheme.medium.color);
} else {
trajectoryColor = p.color(scheme.low.color); // Use custom color for low risk
}
} else {
// MODE 2: DEFAULT TTC SCHEME (Use pre-calculated category from JSON)
switch (lastLog.ttcCategory) {
case 3: trajectoryColor = p.color(localTtcColors.critical); break;
case 2: trajectoryColor = p.color(localTtcColors.high); break;
case 1: trajectoryColor = p.color(localTtcColors.medium); break;
case 0: trajectoryColor = p.color(localTtcColors.low); break;
case -1: trajectoryColor = p.color(localTtcColors.away); break;
default: trajectoryColor = p.color(localTtcColors.default); break;
}
}
p.strokeWeight(1.5);
p.drawingContext.setLineDash([]); // Ensure solid line for moving tracks
p.drawingContext.setLineDash([]);
// Fading trajectory logic
// Fading trajectory logic (works for both modes)
for (let i = 1; i < trajPts.length; i++) {
const alpha = p.map(i, 0, trajPts.length, 50, 255);
trajectoryColor.setAlpha(alpha);
@ -355,7 +349,7 @@ export function drawTrajectories(p, plotScales) {
currPt[0] * plotScales.plotScaleX, currPt[1] * plotScales.plotScaleY
);
}
// --- END: New TTC Coloring Logic ---
// --- END: New Dynamic Coloring Logic ---
}
p.drawingContext.setLineDash([]);

28
steps/src/main.js

@ -198,9 +198,6 @@ clearCacheBtn.addEventListener("click", async () => {
}
});
// 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) {
@ -210,14 +207,13 @@ saveSessionBtn.addEventListener('click', () => {
// Collect all relevant state into a single object.
const sessionState = {
version: 1, // For future compatibility
version: 1,
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,
@ -239,31 +235,25 @@ saveSessionBtn.addEventListener('click', () => {
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.
// --- Dynamic Filename Logic ---
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.
// --- Trigger "Save As" Dialog ---
const a = document.createElement('a');
a.href = url;
// Use the new dynamic filename here. The browser will open a "Save As" dialog.
// This is the key instruction for the browser. It suggests a filename
// and signals that this should open a "Save As" dialog.
a.download = defaultFilename;
document.body.appendChild(a);
a.click(); // Programmatically click the link to start the download.
a.click(); // Programmatically clicking the link triggers the download/save dialog.
// Clean up the temporary link and URL.
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
@ -914,4 +904,4 @@ offsetInput.addEventListener("keydown", (event) => {
// using the new offset value from the input box.
updateFrame(appState.currentFrame, true);
}
});
});

9
steps/src/state.js

@ -29,5 +29,12 @@ export const appState = {
mediaTimeStart: 0,
// Timestamp (from performance.now()) of the last synchronization check
lastSyncTime: 0,
useCustomTtcScheme: false, // Flag to switch between default and custom
customTtcScheme: {
// Default values match the UI
critical: { time: 5, color: "#ff0000" },
high: { time: 10, color: "#ffa500" },
medium: { time: 30, color: "#BA8E23" },
low: { color: "#00ff00" }, // Add this new line
},
};
Loading…
Cancel
Save