Browse Source

feat: Implement robust two-stage file loading and fix race condition

This commit introduces a major improvement to the file loading pipeline, resolving a critical race condition that occurred during fresh loads and drag-and-drop actions. Previously, the application would attempt to initialize data-dependent components (like the speed graph) and manage the loading modal simultaneously, leading to timing issues.

The core of this fix is a new, robust processFilePipeline function in main.js that implements a two-stage video loading process. This decouples data initialization from UI updates, ensuring each occurs at the correct point in the browser's file loading lifecycle.

Key Changes & Bug Fixes:
main.js: Refactored processFilePipeline

Two-Stage Video Loading: The video loading process now uses two distinct event listeners:

loadedmetadata: Fires as soon as the video's duration is known. This event now immediately triggers finalizeSetup(), ensuring that the speedGraphSketch is created with the correct time axis, fixing the blank graph bug.

canplaythrough: Fires only after the video has buffered enough for smooth playback. The resolution of the main videoReadyPromise is tied to this event, guaranteeing the loading modal is hidden at the appropriate time and resolving the "stuck modal" bug.

Explicit Data Synchronization: A final, crucial fix was added to finalizeSetup() to re-synchronize all radar frame timestamps against the video's confirmed start time. This eliminates data mismatches that previously caused NaN errors on fresh loads.

speedGraphSketch.js: Enhanced Robustness

The sketch's draw() and drawTimeIndicator() functions have been made more defensive. They now check that both videoDuration and appState.currentFrame are valid before attempting to render, preventing crashes and NaN errors if the sketch is asked to draw before all data is ready.

modal.js: Improved Loading Modal

The modal logic was updated to support a dedicated loading state with a progress bar, providing better user feedback during the file parsing and video buffering stages.
refactor/modularize
RUSHIL AMBARISH KADU 7 months ago
parent
commit
1c538a6d38
  1. 40
      steps/src/dom.js
  2. 519
      steps/src/main.js
  3. 65
      steps/src/modal.js
  4. 77
      steps/src/p5/speedGraphSketch.js
  5. 1
      steps/src/p5/zoomSketch.js
  6. 423
      zoomsketch-issue/dom.js
  7. 1429
      zoomsketch-issue/main.js
  8. 97
      zoomsketch-issue/modal.js

40
steps/src/dom.js

@ -162,6 +162,46 @@ export function updateFrame(frame, forceVideoSeek) {
} }
//----------------------Reset UI for New file Load----------------------//
// Resets the UI to make sure everything is clean before new files load.
export function resetUIForNewLoad() {
console.log("Resetting UI for new file load.");
// Hide feature toggles
featureToggles.classList.add("hidden");
// Show placeholders
canvasPlaceholder.style.display = 'flex';
videoPlaceholder.classList.remove('hidden');
// Hide video player and overlays
videoPlayer.classList.add('hidden');
videoPlayer.src = ''; // Clear the video source
radarInfoOverlay.classList.add('hidden');
videoInfoOverlay.classList.add('hidden');
// Remove the p5 sketches completely
if (appState.p5_instance) {
appState.p5_instance.remove();
appState.p5_instance = null;
}
if (appState.rawP5_instance) {
appState.rawP5_instance.remove();
appState.rawP5_instance = null;
}
if (appState.zoomSketchInstance) {
appState.zoomSketchInstance.remove();
appState.zoomSketchInstance = null;
}
if (appState.speedGraphInstance) {
appState.speedGraphInstance.remove();
appState.speedGraphInstance = null;
}
// Reset the speed graph container
speedGraphPlaceholder.classList.remove('hidden');
}
//----------------------RESET VISUALIZATION Function----------------------// //----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state. // Resets the visualization to its initial state.
export function resetVisualization() { export function resetVisualization() {

519
steps/src/main.js

@ -17,7 +17,12 @@
// =========================================================================================================== // ===========================================================================================================
import { zoomSketch } from "./p5/zoomSketch.js"; import { zoomSketch } from "./p5/zoomSketch.js";
import { showModal, updateModalProgress } from "./modal.js"; // Modify this import
import {
showModal,
hideModal,
updateLoadingModal,
showLoadingModal,
} from "./modal.js"; // Modify this import
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";
@ -96,6 +101,7 @@ import {
fullscreenExitIcon, fullscreenExitIcon,
menuScrim, menuScrim,
toggleConfirmedOnly, toggleConfirmedOnly,
resetUIForNewLoad,
} from "./dom.js"; } from "./dom.js";
import { initializeTheme } from "./theme.js"; import { initializeTheme } from "./theme.js";
@ -106,6 +112,244 @@ let seekDebounceTimer = null; //timeline slider variables.
let lastScrollTime = 0; //timeline slider variables. let lastScrollTime = 0; //timeline slider variables.
let scrollSpeed = 0; //timeline slider variables. let scrollSpeed = 0; //timeline slider variables.
// --- [START] CORRECTED UNIFIED FILE LOADING LOGIC ---
// These variables will hold the file objects during the loading process.
let jsonFileToLoad = null;
let videoFileToLoad = null;
/**
* This is the main handler for both manual clicks and drag-and-drop.
* It identifies the files and triggers the unified processing pipeline.
*/
function handleFiles(files) {
// Reset the UI and clear any old data to prepare for a new session
resetUIForNewLoad();
appState.vizData = null;
// Identify the JSON and Video files from the list of files provided
Array.from(files).forEach((file) => {
if (file.name.endsWith(".json")) {
jsonFileToLoad = file;
} else if (file.type.startsWith("video/")) {
videoFileToLoad = file;
}
});
// Start the main loading process if we have at least one valid file.
if (jsonFileToLoad || videoFileToLoad) {
processFilePipeline();
}
}
// Wire up the manual file inputs to the new handler
jsonFileInput.addEventListener("change", (event) =>
handleFiles(event.target.files)
);
videoFileInput.addEventListener("change", (event) =>
handleFiles(event.target.files)
);
// Wire up the drag-and-drop functionality
const dropZone = document.querySelector("main");
dropZone.addEventListener("dragover", (event) => {
event.preventDefault();
dropZone.style.border = "2px dashed #3b82f6";
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.border = "none";
});
dropZone.addEventListener("drop", (event) => {
event.preventDefault();
dropZone.style.border = "none";
handleFiles(event.dataTransfer.files);
});
async function processFilePipeline() {
// 1. Show the unified loading modal.
showLoadingModal("Starting file load...");
let _parsedJsonData = null;
// 2. Handle JSON Parsing FIRST (if a JSON file is present)
if (jsonFileToLoad) {
appState.jsonFilename = jsonFileToLoad.name;
localStorage.setItem("jsonFilename", appState.jsonFilename);
await saveFileWithMetadata("json", jsonFileToLoad);
calculateAndSetOffset();
const worker = new Worker("./src/parser.worker.js");
const parsedData = await new Promise((resolve, reject) => {
worker.onmessage = (e) => {
const { type, data, percent, message } = e.data;
if (type === "progress") {
updateLoadingModal(percent * 0.8, `Parsing JSON (${percent}%)...`);
} else if (type === "complete") {
worker.terminate();
resolve(data);
} else if (type === "error") {
worker.terminate();
reject(new Error(message));
}
};
worker.postMessage({ file: jsonFileToLoad });
});
_parsedJsonData = parsedData;
const result = await parseVisualizationJson(
parsedData,
appState.radarStartTimeMs,
appState.videoStartDate
);
if (result.error) {
hideModal();
showModal(result.error);
return;
}
appState.vizData = result.data;
appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr;
}
// 3. Handle Video Loading SECOND, with two-stage initialization
if (videoFileToLoad) {
videoPlayer.addEventListener(
"durationchange",
() => {
if (
videoPlayer.duration > 0 &&
appState.speedGraphInstance &&
appState.vizData
) {
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
}
},
{ once: true }
);
let spinnerInterval; // Declare here to be accessible in all scopes
// This single promise manages the entire video loading lifecycle.
const videoReadyPromise = new Promise((resolve, reject) => {
// Define cleanup logic to remove listeners and stop the spinner
const cleanup = () => {
clearInterval(spinnerInterval);
videoPlayer.removeEventListener("loadedmetadata", onMetadataLoaded);
videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough);
videoPlayer.removeEventListener("error", onError);
};
// STAGE 1: Fired when video duration is known.
const onMetadataLoaded = () => {
updateLoadingModal(95, "Finalizing visualization...");
// This is the key fix: initialize data-dependent sketches immediately.
finalizeSetup(_parsedJsonData);
};
// STAGE 2: Fired when video is buffered enough to play.
const onCanPlayThrough = () => {
cleanup();
resolve(); // Resolve the promise, allowing the pipeline to complete.
};
// Handle any loading errors
const onError = (e) => {
console.error("Video loading error:", e);
cleanup();
reject(e);
};
// Attach the event listeners
videoPlayer.addEventListener("loadedmetadata", onMetadataLoaded, {
once: true,
});
videoPlayer.addEventListener("canplaythrough", onCanPlayThrough, {
once: true,
});
videoPlayer.addEventListener("error", onError, { once: true });
});
// Set up file metadata and start the simulated progress spinner
appState.videoFilename = videoFileToLoad.name;
localStorage.setItem("videoFilename", appState.videoFilename);
await saveFileWithMetadata("video", videoFileToLoad);
calculateAndSetOffset();
const spinnerChars = ["|", "/", "-", "\\"];
let spinnerIndex = 0;
spinnerInterval = setInterval(() => {
const spinnerText = spinnerChars[spinnerIndex % spinnerChars.length];
updateLoadingModal(85, `Loading video ${spinnerText}`);
spinnerIndex++;
}, 150);
// Trigger the video loading process
setupVideoPlayer(URL.createObjectURL(videoFileToLoad));
// Await the promise, which resolves only after 'canplaythrough' fires.
await videoReadyPromise;
// 4. Finalize the UI by hiding the modal
updateLoadingModal(100, "Complete!");
setTimeout(hideModal, 300);
} else {
// If NO video was loaded, we must still finalize the setup and hide the modal.
updateLoadingModal(95, "Finalizing visualization...");
finalizeSetup(_parsedJsonData); // Setup with only JSON data
setTimeout(() => {
updateLoadingModal(100, "Complete!");
setTimeout(hideModal, 300);
}, 200);
}
}
function finalizeSetup(_parsedJsonData) {
// Make sure the canvas placeholder is hidden and toggles are visible
canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden");
// --- START OF THE FIX ---
// This is the critical step. Before we do anything else, we loop through the
// radar data and recalculate the relative timestamp for every single frame.
// This ensures the data is perfectly synced to the video's confirmed timeline.
if (appState.vizData && appState.videoStartDate) {
appState.vizData.radarFrames.forEach((frame) => {
frame.timestampMs =
appState.radarStartTimeMs +
frame.timestamp -
appState.videoStartDate.getTime();
});
}
// --- END OF THE FIX ---
// Create the p5 instances
if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch);
}
if (!appState.zoomSketchInstance) {
appState.zoomSketchInstance = new p5(zoomSketch, "zoom-canvas-container");
}
// Setup the speed graph if we have the necessary data
if (appState.vizData) {
speedGraphPlaceholder.classList.add("hidden");
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
// The previous logic for setting the frame and redrawing was correct.
// It failed because the underlying timestamp data was wrong.
resetVisualization();
appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration);
appState.speedGraphInstance.redraw();
}
// Update SNR inputs now that data is loaded
if (appState.vizData) {
snrMinInput.value = appState.globalMinSnr.toFixed(1);
snrMaxInput.value = appState.globalMaxSnr.toFixed(1);
}
}
// 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;
@ -159,7 +403,6 @@ function loadVideoWithProgress(videoObject) {
() => { () => {
// 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.");
appState.vizData.radarFrames.forEach((frame) => { appState.vizData.radarFrames.forEach((frame) => {
frame.timestampMs = frame.timestampMs =
appState.radarStartTimeMs + appState.radarStartTimeMs +
@ -275,10 +518,6 @@ saveSessionBtn.addEventListener("click", () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}); });
/**
* A callback that runs for every new video frame presented to the screen.
* It calculates the time since the last frame to measure video performance.
*/
function videoFrameCallback(now, metadata) { function videoFrameCallback(now, metadata) {
// 'now' is a high-resolution timestamp provided by the browser // 'now' is a high-resolution timestamp provided by the browser
if (appState.lastVideoFrameTime > 0) { if (appState.lastVideoFrameTime > 0) {
@ -419,112 +658,6 @@ document.addEventListener("fullscreenchange", () => {
} }
}); });
// In main.js, REPLACE your existing jsonFileInput event listener with this entire block:
jsonFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
appState.jsonFilename = file.name;
localStorage.setItem("jsonFilename", appState.jsonFilename);
calculateAndSetOffset();
saveFileWithMetadata("json", file); // We still cache the raw file
// 1. Show the modal with the progress bar
showModal("Parsing large JSON file...", false, true);
updateModalProgress(0);
// 2. Create a new Worker from our script
const worker = new Worker("./src/parser.worker.js");
// 3. Set up listeners for messages FROM the worker
worker.onmessage = async (e) => {
const { type, data, message, percent } = e.data;
if (type === "progress") {
updateModalProgress(percent);
} else if (type === "complete") {
updateModalProgress(100);
const result = await parseVisualizationJson(
data,
appState.radarStartTimeMs,
appState.videoStartDate
);
if (result.error) {
showModal(result.error);
worker.terminate(); // Terminate worker on error
return;
}
if (appState.p5_instance) {
appState.p5_instance.remove();
appState.p5_instance = null;
}
if (appState.speedGraphInstance) {
appState.speedGraphInstance.remove();
appState.speedGraphInstance = null;
speedGraphPlaceholder.classList.remove("hidden");
}
appState.vizData = result.data;
appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr;
snrMinInput.value = appState.globalMinSnr.toFixed(1);
snrMaxInput.value = appState.globalMaxSnr.toFixed(1);
resetVisualization();
canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden");
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
);
}
// --- END: This is the new, corrected logic ---
document.getElementById("modal-ok-btn").click();
worker.terminate();
} else if (type === "error") {
showModal(message);
worker.terminate();
}
};
// 4. Send the file TO the worker to start the job
worker.postMessage({ file: file });
});
// Event listener for video file input change.
// In src/main.js, REPLACE the videoFileInput event listener with this:
videoFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
appState.videoFilename = file.name;
localStorage.setItem("videoFilename", appState.videoFilename);
saveFileWithMetadata("video", file);
calculateAndSetOffset();
loadVideoWithProgress(file);
// Start the performance monitoring loop as soon as a video is attached.
videoPlayer.requestVideoFrameCallback(videoFrameCallback);
});
// 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");
@ -811,7 +944,7 @@ document.addEventListener("keydown", (event) => {
"s", "s",
"m", "m",
"q", "q",
"c"
"c",
]; ];
if (!appState.vizData || !recognizedKeys.includes(key)) { if (!appState.vizData || !recognizedKeys.includes(key)) {
@ -873,12 +1006,12 @@ document.addEventListener("keydown", (event) => {
resetVisualization(); resetVisualization();
} }
if (key === "c") { if (key === "c") {
appState.isRawOnlyMode = !appState.isRawOnlyMode;
if(appState.p5_instance) {
appState.p5_instance.redraw();
appState.isRawOnlyMode = !appState.isRawOnlyMode;
if (appState.p5_instance) {
appState.p5_instance.redraw();
} }
} }
if (key === "p") { if (key === "p") {
togglePredictedPos.click(); togglePredictedPos.click();
appState.p5_instance.redraw(); appState.p5_instance.redraw();
@ -917,11 +1050,10 @@ function calculateAndSetOffset() {
videoTimestampInfo.timestampStr, videoTimestampInfo.timestampStr,
videoTimestampInfo.format videoTimestampInfo.format
); );
if (appState.videoStartDate)
console.log(
`Video start date set to: ${appState.videoStartDate.toISOString()}`
);
if (appState.videoStartDate){
};
} }
if (jsonTimestampInfo) { if (jsonTimestampInfo) {
const jsonDate = parseTimestamp( const jsonDate = parseTimestamp(
jsonTimestampInfo.timestampStr, jsonTimestampInfo.timestampStr,
@ -942,146 +1074,37 @@ function calculateAndSetOffset() {
} }
} }
// Application Initialization
// FILE: steps/src/main.js
// REPLACE the entire 'document.addEventListener("DOMContentLoaded", ...)' block with this:
// --- [START] CORRECTED INITIALIZATION LOGIC ---
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initializeTheme(); initializeTheme();
console.log("DEBUG: DOMContentLoaded fired. Starting session load.");
initDB(async () => { initDB(async () => {
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);
}
console.log("Database initialized. Checking for cached session...");
// 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"); appState.jsonFilename = localStorage.getItem("jsonFilename");
appState.videoFilename = localStorage.getItem("videoFilename");
calculateAndSetOffset();
const videoBlob = await loadFreshFileFromDB(
"video",
appState.videoFilename
);
const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename);
console.log(
"DEBUG: Freshness checks complete. Proceeding with valid data."
);
if (appState.jsonFilename) {
const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename);
const videoBlob = await loadFreshFileFromDB(
"video",
appState.videoFilename
);
const finalizeSetup = async (parsedJson) => {
if (parsedJson) {
const result = await parseVisualizationJson(
parsedJson,
appState.radarStartTimeMs,
appState.videoStartDate
if (jsonBlob) {
console.log("Cached session found. Starting auto-reload...");
// Use the handleFiles function to trigger the pipeline with cached blobs
handleFiles([jsonBlob, videoBlob].filter(Boolean)); // .filter(Boolean) removes null videoBlob if it doesn't exist
} else {
console.log(
"Cached session is stale or missing files. Ready for manual load."
); );
if (!result.error) {
appState.vizData = result.data;
// 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);
}
}
if (appState.vizData) {
resetVisualization();
canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden");
if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch);
}
if (!appState.zoomSketchInstance) {
appState.zoomSketchInstance = new p5(zoomSketch, 'zoom-canvas-container');
}
} }
//document.getElementById("zoom-panel").style.display = "none";
};
if (jsonBlob) {
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);
document.getElementById("modal-ok-btn").click();
worker.terminate();
loadVideoWithProgress(videoBlob);
} else if (type === "error") {
showModal(message);
worker.terminate();
}
};
worker.postMessage({ file: jsonBlob });
} else { } else {
await finalizeSetup(null);
loadVideoWithProgress(videoBlob);
console.log("No previous session found. Ready for manual file load.");
} }
}); });
}); });
// --- [END] CORRECTED INITIALIZATION LOGIC ---
// In src/main.js, add this new event listener // In src/main.js, add this new event listener
offsetInput.addEventListener("keydown", (event) => { offsetInput.addEventListener("keydown", (event) => {

65
steps/src/modal.js

@ -3,57 +3,65 @@ import {
modalContainer, modalContainer,
modalOverlay, modalOverlay,
modalContent, modalContent,
} from "./dom.js";
// First, import the new DOM elements at the top
import {
modalText, modalText,
//...
modalOkBtn, modalOkBtn,
modalProgressContainer, // Add this
modalProgressBar, // Add this
modalProgressText, // Add this
modalProgressContainer,
modalProgressBar,
modalProgressText,
} from "./dom.js"; } from "./dom.js";
// --- Custom Modal Logic --- //
// Variable to store the resolve function of the Promise, allowing the modal to return a value.
let modalResolve = null; let modalResolve = null;
export function showModal(message, isConfirm = false, showProgress = false) {
export function showModal(message, isConfirm = false) {
return new Promise((resolve) => { return new Promise((resolve) => {
// Set the message text for the modal.
modalText.textContent = message; modalText.textContent = message;
// Show/hide the cancel button based on whether it's a confirmation modal.
// This line correctly shows the "Cancel" button only when needed.
modalCancelBtn.classList.toggle("hidden", !isConfirm); modalCancelBtn.classList.toggle("hidden", !isConfirm);
modalProgressContainer.classList.toggle("hidden", !showProgress);
// --- THIS IS THE FIX ---
// This ensures the "OK" button is always visible for this modal.
modalOkBtn.classList.remove("hidden");
modalProgressContainer.classList.add("hidden");
// Make the modal container visible.
modalContainer.classList.remove("hidden"); modalContainer.classList.remove("hidden");
// Add a slight delay for CSS transitions to take effect, making the modal appear smoothly.
setTimeout(() => { setTimeout(() => {
modalOverlay.classList.remove("opacity-0"); modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95"); modalContent.classList.remove("scale-95");
}, 10); }, 10);
// Store the resolve function to be called when the modal is closed.
modalResolve = resolve; modalResolve = resolve;
}); });
} }
// Add this new exported function to update the progress bar
export function updateModalProgress(percent) {
// A new function specifically for the loading modal
export function showLoadingModal(message) {
modalText.textContent = message;
modalOkBtn.classList.add('hidden');
modalCancelBtn.classList.add('hidden');
modalProgressContainer.classList.remove('hidden');
modalProgressBar.style.width = '0%';
modalProgressText.textContent = 'Initializing...';
modalContainer.classList.remove("hidden");
setTimeout(() => {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
}
// A new function to update the progress bar and text
export function updateLoadingModal(percent, message) {
if (modalProgressBar && modalProgressText) { if (modalProgressBar && modalProgressText) {
const p = Math.round(percent);
const p = Math.max(0, Math.min(100, Math.round(percent))); // Clamp between 0-100
modalProgressBar.style.width = `${p}%`; modalProgressBar.style.width = `${p}%`;
modalProgressText.textContent =
p < 100 ? `Parsing... ${p}%` : "Finalizing...";
modalProgressText.textContent = message;
} }
} }
// Hides the modal and resolves the Promise with the given value.
function hideModal(value) {
// The hideModal function now also resets the progress bar
export function hideModal(value) {
modalOverlay.classList.add("opacity-0"); modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95"); modalContent.classList.add("scale-95");
setTimeout(() => { setTimeout(() => {
modalContainer.classList.add("hidden"); modalContainer.classList.add("hidden");
// Reset progress bar for the next time
if (modalProgressContainer && modalProgressBar && modalProgressText) { if (modalProgressContainer && modalProgressBar && modalProgressText) {
modalProgressContainer.classList.add("hidden"); modalProgressContainer.classList.add("hidden");
modalProgressBar.style.width = "0%"; modalProgressBar.style.width = "0%";
@ -63,10 +71,7 @@ function hideModal(value) {
}, 200); }, 200);
} }
//----------------------Modal Event Listeners----------------------//
// Event listener for the "OK" button. Resolves the modal Promise with 'true'.
// Event listeners remain the same
modalOkBtn.addEventListener("click", () => hideModal(true)); modalOkBtn.addEventListener("click", () => hideModal(true));
// Event listener for the "Cancel" button. Resolves the modal Promise with 'false'.
modalCancelBtn.addEventListener("click", () => hideModal(false)); modalCancelBtn.addEventListener("click", () => hideModal(false));
// Event listener for clicking on the modal overlay (outside the content). Resolves the modal Promise with 'false'.
modalOverlay.addEventListener("click", () => hideModal(false));
modalOverlay.addEventListener("click", () => hideModal(false));

77
steps/src/p5/speedGraphSketch.js

@ -65,8 +65,20 @@ export const speedGraphSketch = function (p) {
} }
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 y = b.map(frame.canVehSpeed_kmph, 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);
} }
} }
@ -80,9 +92,21 @@ 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);
} }
} }
@ -119,8 +143,9 @@ export const speedGraphSketch = function (p) {
}; };
p.setData = function (radarData, duration) { p.setData = function (radarData, duration) {
if (!radarData || !radarData.radarFrames) return; if (!radarData || !radarData.radarFrames) return;
videoDuration = duration;
videoDuration = duration; // Accept duration, even if it's 0 or NaN initially
let speeds = []; let speeds = [];
if (radarData && radarData.radarFrames) { if (radarData && radarData.radarFrames) {
@ -135,25 +160,51 @@ export const speedGraphSketch = function (p) {
speeds.push(...canSpeeds); speeds.push(...canSpeeds);
} }
minSpeed = speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0;
maxSpeed = speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 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;
if (maxSpeed <= 0) maxSpeed = 10; if (maxSpeed <= 0) maxSpeed = 10;
if (minSpeed >= 0) minSpeed = 0; if (minSpeed >= 0) minSpeed = 0;
p.drawStaticGraphToBuffer(radarData);
p.redraw();
// *** KEY CHANGE ***
// Only try to draw the static graph if the duration is valid.
if (videoDuration > 0) {
p.drawStaticGraphToBuffer(radarData);
}
//p.redraw();
}; };
p.draw = function () { p.draw = function () {
if (!videoDuration) return;
// *** KEY CHANGE ***
// If duration is not ready, show a waiting message and stop
if (!videoDuration || videoDuration <= 0) {
const isDark = document.documentElement.classList.contains("dark");
p.background(isDark ? [55, 65, 81] : 255);
p.fill(isDark ? 200 : 100);
p.textAlign(p.CENTER, p.CENTER);
p.text("Waiting for video duration...", p.width / 2, p.height / 2);
return;
}
p.image(staticBuffer, 0, 0); p.image(staticBuffer, 0, 0);
drawTimeIndicator(); drawTimeIndicator();
}; };
function drawTimeIndicator() { function drawTimeIndicator() {
// This new, more robust check is the fix. It ensures that the video duration is valid AND
// the main application has initialized the currentFrame before attempting to draw.
if (
!videoDuration ||
videoDuration <= 0 ||
appState.currentFrame === null ||
appState.currentFrame === undefined
) {
return; // Stop here if the state is not ready
}
// Get the current frame's data as the single source of truth // Get the current frame's data as the single source of truth
const frameData = appState.vizData.radarFrames[appState.currentFrame]; const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return; // Exit if data isn't ready
if (!frameData) return; // Exit if data for the specific frame isn't ready
// Calculate the X position from the current frame's precise timestamp // Calculate the X position from the current frame's precise timestamp
const currentTimeSec = frameData.timestampMs / 1000.0; const currentTimeSec = frameData.timestampMs / 1000.0;
@ -186,9 +237,9 @@ export const speedGraphSketch = function (p) {
speedGraphContainer.offsetHeight speedGraphContainer.offsetHeight
); );
staticBuffer = p.createGraphics(p.width, p.height); staticBuffer = p.createGraphics(p.width, p.height);
if (appState.vizData && videoDuration) {
if (appState.vizData && videoDuration > 0) {
p.drawStaticGraphToBuffer(appState.vizData); p.drawStaticGraphToBuffer(appState.vizData);
} }
p.redraw(); p.redraw();
}; };
};
};

1
steps/src/p5/zoomSketch.js

@ -284,7 +284,6 @@ export const zoomSketch = function (p) {
appState.zoomFactor = 4; // Set a default zoom factor in the global state appState.zoomFactor = 4; // Set a default zoom factor in the global state
p.setup = function () { p.setup = function () {
console.log("zoomSketch: Setup function has been called."); //debug
p.noLoop(); p.noLoop();
}; };

423
zoomsketch-issue/dom.js

@ -0,0 +1,423 @@
import { appState } from "./state.js";
import { formatUTCTime } from "./utils.js";
// Also import VIDEO_FPS from constants
import { VIDEO_FPS } from "./constants.js";
// --- DOM Element References --- //
export const themeToggleBtn = document.getElementById("theme-toggle");
export const canvasContainer = document.getElementById("canvas-container");
export const canvasPlaceholder = document.getElementById("canvas-placeholder");
export const videoPlayer = document.getElementById("video-player");
export const videoPlaceholder = document.getElementById("video-placeholder");
export const loadJsonBtn = document.getElementById("load-json-btn");
export const loadVideoBtn = document.getElementById("load-video-btn");
export const loadCanBtn = document.getElementById("load-can-btn");
export const jsonFileInput = document.getElementById("json-file-input");
export const videoFileInput = document.getElementById("video-file-input");
export const canFileInput = document.getElementById("can-file-input");
export const playPauseBtn = document.getElementById("play-pause-btn");
export const stopBtn = document.getElementById("stop-btn");
export const timelineSlider = document.getElementById("timeline-slider");
export const frameCounter = document.getElementById("frame-counter");
export const offsetInput = document.getElementById("offset-input");
export const speedSlider = document.getElementById("speed-slider");
export const speedDisplay = document.getElementById("speed-display");
export const featureToggles = document.getElementById("feature-toggles");
export const toggleSnrColor = document.getElementById("toggle-snr-color");
export const toggleClusterColor = document.getElementById("toggle-cluster-color");
export const toggleInlierColor = document.getElementById("toggle-inlier-color");
export const toggleStationaryColor = document.getElementById("toggle-stationary-color");
export const toggleVelocity = document.getElementById("toggle-velocity");
export const toggleTracks = document.getElementById("toggle-tracks");
export const toggleEgoSpeed = document.getElementById("toggle-ego-speed");
export const toggleFrameNorm = document.getElementById("toggle-frame-norm");
export const toggleDebugOverlay = document.getElementById("toggle-debug-overlay");
export const egoSpeedDisplay = document.getElementById("ego-speed-display");
export const canSpeedDisplay = document.getElementById("can-speed-display");
export const debugOverlay = document.getElementById("debug-overlay");
export const toggleDebug2Overlay = document.getElementById("toggle-debug2-overlay");
export const snrMinInput = document.getElementById("snr-min-input");
export const snrMaxInput = document.getElementById("snr-max-input");
export const applySnrBtn = document.getElementById("apply-snr-btn");
export const autoOffsetIndicator = document.getElementById("auto-offset-indicator");
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 modalContainer = document.getElementById("modal-container");
export const modalOverlay = document.getElementById("modal-overlay");
export const modalContent = document.getElementById("modal-content");
export const modalText = document.getElementById("modal-text");
export const modalOkBtn = document.getElementById("modal-ok-btn");
export const modalCancelBtn = document.getElementById("modal-cancel-btn");
export const toggleCloseUp = document.getElementById("toggle-close-up");
export const togglePredictedPos = document.getElementById("toggle-predicted-pos");
export const toggleCovariance = document.getElementById("toggle-covariance");
export const modalProgressContainer = document.getElementById("modal-progress-container");
export const modalProgressBar = document.getElementById("modal-progress-bar");
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");
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");
export const collapsibleMenu = document.getElementById("collapsible-menu");
export const toggleMenuBtn = document.getElementById("toggle-menu-btn");
export const fullscreenBtn = document.getElementById("fullscreen-btn");
export const mainContent = document.querySelector("main");
export const closeMenuBtn = document.getElementById("close-menu-btn");
export const fullscreenEnterIcon = document.getElementById("fullscreen-enter-icon");
export const fullscreenExitIcon = document.getElementById("fullscreen-exit-icon");
export const menuScrim = document.getElementById("menu-scrim");
export const toggleConfirmedOnly = document.getElementById("toggle-confirmed-only");
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.
export function updateFrame(frame, forceVideoSeek) {
const startTime = performance.now(); //start emasuring timer of performance.
if (
!appState.vizData ||
frame < 0 ||
frame >= appState.vizData.radarFrames.length
)
// Exit if no visualization data or invalid frame.
return; // Exit if no visualization data or invalid frame
appState.currentFrame = frame;
timelineSlider.value = appState.currentFrame;
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${
appState.vizData.radarFrames.length
}`;
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (toggleEgoSpeed.checked && frameData) {
// Update ego speed display if enabled.
const egoVy_kmh = (frameData.egoVelocity[1] * 3.6).toFixed(1); // Convert m/s to km/h and format
egoSpeedDisplay.textContent = `Ego: ${egoVy_kmh} km/h`;
egoSpeedDisplay.classList.remove("hidden");
} else {
egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display.
}
// --- 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 (
forceVideoSeek &&
videoPlayer.src &&
videoPlayer.readyState > 1 &&
appState.videoStartDate &&
frameData
) {
const offsetMs = parseFloat(offsetInput.value) || 0;
const targetRadarTimeMs = frameData.timestampMs;
const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000;
if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) {
// Ensure target time is within video duration
if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) {
// Check for significant drift
videoPlayer.currentTime = targetVideoTimeSec; // Seek video if drift is significant
}
// MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime
timeForUpdates = targetVideoTimeSec; // Update time for subsequent UI updates
}
} // End of forceVideoSeek block
if (!appState.isPlaying) {
// MODIFIED: Use our new synchronized time variable
updatePersistentOverlays(timeForUpdates);
}
// --- End of fix ---
if (appState.p5_instance) appState.p5_instance.redraw(); // Redraw radar sketch
if (appState.speedGraphInstance && !appState.isPlaying)
// Redraw speed graph if not playing.
appState.speedGraphInstance.redraw();
const endTime = performance.now();
appState.lastFrameRenderTime = endTime - startTime; // <-- End timer and update state
}
//----------------------Reset UI for New file Load----------------------//
// Resets the UI to make sure everything is clean before new files load.
export function resetUIForNewLoad() {
console.log("Resetting UI for new file load.");
// Hide feature toggles
featureToggles.classList.add("hidden");
// Show placeholders
canvasPlaceholder.style.display = 'flex';
videoPlaceholder.classList.remove('hidden');
// Hide video player and overlays
videoPlayer.classList.add('hidden');
videoPlayer.src = ''; // Clear the video source
radarInfoOverlay.classList.add('hidden');
videoInfoOverlay.classList.add('hidden');
// Remove the p5 sketches completely
if (appState.p5_instance) {
appState.p5_instance.remove();
appState.p5_instance = null;
}
if (appState.rawP5_instance) {
appState.rawP5_instance.remove();
appState.rawP5_instance = null;
}
if (appState.zoomSketchInstance) {
appState.zoomSketchInstance.remove();
appState.zoomSketchInstance = null;
}
if (appState.speedGraphInstance) {
appState.speedGraphInstance.remove();
appState.speedGraphInstance = null;
}
// Reset the speed graph container
speedGraphPlaceholder.classList.remove('hidden');
}
//----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state.
export function resetVisualization() {
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
const numFrames = appState.vizData.radarFrames.length;
timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0;
updateFrame(0, true); // Update to the first frame and force video seek
}
//----------------------CAN DISPLAY UPDATE Function----------------------//
// Updates the CAN speed display based on the current media time.
//----------------------DEBUG OVERLAY UPDATE Function----------------------//
// Updates the debug overlay with various synchronization and time information.
export function updateDebugOverlay(currentMediaTime) {
// Check the state of both debug toggles
const isDebug1Visible = toggleDebugOverlay.checked;
const isDebug2Visible = toggleDebug2Overlay.checked;
// If neither is checked, hide the overlay and stop
if (!isDebug1Visible && !isDebug2Visible) {
debugOverlay.classList.add("hidden"); // Hide debug overlay
return;
}
// If at least one is checked, show the overlay
debugOverlay.classList.remove("hidden"); // Show debug overlay.
let content = [];
// --- Logic for the original debug overlay ---
if (isDebug1Visible) {
content.push(`--- Basic Info ---`);
if (appState.videoStartDate) {
const videoAbsoluteTimeMs =
appState.videoStartDate.getTime() + currentMediaTime * 1000;
content.push(`Media Time (s): ${currentMediaTime.toFixed(3)}`);
content.push(`Video Frame: ${Math.floor(currentMediaTime * VIDEO_FPS)}`);
content.push(
`Vid Abs Time: ${new Date(videoAbsoluteTimeMs)
.toISOString()
.split("T")[1]
.replace("Z", "")}`
); // Format and display video absolute time
} else {
content.push("Video not loaded..."); // Indicate video not loaded.
}
if (
appState.vizData &&
appState.vizData.radarFrames[appState.currentFrame]
) {
content.push(`Radar Frame: ${appState.currentFrame + 1}`);
const frameTime =
appState.vizData.radarFrames[appState.currentFrame].timestampMs;
content.push(
`Radar Abs Time: ${new Date(
appState.videoStartDate.getTime() + frameTime
)
.toISOString()
.split("T")[1]
.replace("Z", "")}`
); // Format and display radar absolute time
}
}
// --- Logic for the new advanced debug overlay ---
if (isDebug2Visible) {
content.push(`--- Sync Diagnostics ---`);
if (
appState.videoStartDate &&
appState.vizData &&
appState.vizData.radarFrames[appState.currentFrame]
) {
// --- START: Corrected Debug Logic ---
const currentRadarFrame =
appState.vizData.radarFrames[appState.currentFrame];
const targetRadarTimeMs = currentRadarFrame.timestampMs;
const offsetMs = parseFloat(offsetInput.value) || 0; // Read the current offset
// Make the drift calculation "offset-aware"
const driftMs = currentMediaTime * 1000 + offsetMs - targetRadarTimeMs;
// --- END: Corrected Debug Logic ---
// Style the drift value to be green if sync is good, and red if it's off.
const driftColor = Math.abs(driftMs) > 40 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
content.push(`Video Time (s): ${currentMediaTime.toFixed(3)}`); // Display current video time
content.push(`Target Radar Time (ms): ${targetRadarTimeMs.toFixed(0)}`);
content.push(`Drift (ms): <b style="color: ${driftColor};">${driftMs.toFixed(0)}</b>`);
content.push(`Video Start Time: ${appState.videoStartDate.toISOString()}`);
content.push(`Radar Start Time: ${new Date(appState.radarStartTimeMs).toISOString()}`);
content.push(`Calculated Offset (ms): ${offsetInput.value}`); // Display calculated offset.
const renderTime = appState.lastFrameRenderTime;
// Color is green if render time is under 33ms (~30fps budget), otherwise red
const renderTimeColor = renderTime > 33 ? "#FF6347" : "#98FB98";
content.push(`Frame Render Time: <b style="color: ${renderTimeColor};">${renderTime.toFixed(1)}ms</b>`);
const videoRenderTime = appState.videoFrameRenderTime;
// Color is green if render time is under 34ms (~30fps), otherwise red
const videoRenderTimeColor = videoRenderTime > 34 ? "#FF6347" : "#98FB98";
content.push(`Video Frame Time: <b style="color: ${videoRenderTimeColor};">${videoRenderTime.toFixed(1)}ms</b>`);
} else {
content.push("Load video and radar data to see sync info."); // Prompt to load data.
}
}
debugOverlay.innerHTML = content.join("<br>"); // Update debug overlay content.
}
// This function checks the state of the color toggles and returns the active mode.
function getCurrentColorMode() {
if (toggleSnrColor.checked) return "Color by SNR (1)";
if (toggleClusterColor.checked) return "Color by Cluster (2)";
if (toggleInlierColor.checked) return "Color by Inlier (3)";
if (toggleStationaryColor.checked) return "Color by Stationary (4)";
return "Default"; // The default mode when no specific color toggle is checked
}
export function updatePersistentOverlays(currentMediaTime) {
// If we don't have the necessary data, hide the overlays and exit.
const isDebug1Visible = toggleDebugOverlay.checked;
const isDebug2Visible = toggleDebug2Overlay.checked;
if (!appState.vizData || !appState.videoStartDate) {
radarInfoOverlay.classList.add("hidden");
videoInfoOverlay.classList.add("hidden");
return;
}
if (isDebug1Visible && isDebug2Visible) {
radarInfoOverlay.classList.add("hidden");
videoInfoOverlay.classList.add("hidden");
return;
}
if(isDebug1Visible || isDebug2Visible){
videoInfoOverlay.classList.add("hidden");
return;
}
// Otherwise, make sure they are visible.
radarInfoOverlay.classList.remove("hidden");
videoInfoOverlay.classList.remove("hidden");
// --- Update Radar Overlay ---
const currentRadarFrame = appState.vizData.radarFrames[appState.currentFrame];
const frameData = appState.vizData.radarFrames[appState.currentFrame];
const motionState = frameData.motionState;
if (currentRadarFrame) {
const absRadarTime = new Date(
appState.videoStartDate.getTime() + currentRadarFrame.timestampMs
);
const targetRadarTimeMs = currentRadarFrame.timestampMs;
const offsetMs = parseFloat(offsetInput.value) || 0;
const driftMs = currentMediaTime * 1000 + offsetMs - targetRadarTimeMs;
const driftColor = Math.abs(driftMs) > 50 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
const colorMode = getCurrentColorMode();
radarInfoOverlay.innerHTML = `
Frame: ${appState.currentFrame + 1}
Motion State: ${motionState}
| Abs Time: ${formatUTCTime(absRadarTime)}
| Color Mode: <b>${colorMode}</b>
| Drift: <b style="color: ${driftColor};">${driftMs.toFixed(
0
)}ms </b>
`;
}
// --- Update Video Overlay ---
const absVideoTime = new Date(
appState.videoStartDate.getTime() + currentMediaTime * 1000
);
const videoFrame = Math.floor(currentMediaTime * VIDEO_FPS);
//console.warn('Could not load radarframes ', appState.vizData.radarFrames) console warning for reference
videoInfoOverlay.innerHTML = `
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);
});

1429
zoomsketch-issue/main.js
File diff suppressed because it is too large
View File

97
zoomsketch-issue/modal.js

@ -0,0 +1,97 @@
import {
modalCancelBtn,
modalContainer,
modalOverlay,
modalContent,
modalText,
modalOkBtn,
modalProgressContainer,
modalProgressBar,
modalProgressText,
} from "./dom.js";
let modalResolve = null;
// The showModal function is now simpler.
/* export function showModal(message, isConfirm = false) {
return new Promise((resolve) => {
modalText.textContent = message;
modalCancelBtn.classList.toggle("hidden");
modalOkBtn.classList.toggle("hidden", isConfirm);
modalProgressContainer.classList.add("hidden"); // Hide progress by default
modalContainer.classList.remove("hidden");
setTimeout(() => {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
modalResolve = resolve;
});
} */
export function showModal(message, isConfirm = false) {
return new Promise((resolve) => {
modalText.textContent = message;
// This line correctly shows the "Cancel" button only when needed.
modalCancelBtn.classList.toggle("hidden", !isConfirm);
// --- THIS IS THE FIX ---
// This ensures the "OK" button is always visible for this modal.
modalOkBtn.classList.remove("hidden");
modalProgressContainer.classList.add("hidden");
modalContainer.classList.remove("hidden");
setTimeout(() => {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
modalResolve = resolve;
});
}
// A new function specifically for the loading modal
export function showLoadingModal(message) {
modalText.textContent = message;
modalOkBtn.classList.add('hidden');
modalCancelBtn.classList.add('hidden');
modalProgressContainer.classList.remove('hidden');
modalProgressBar.style.width = '0%';
modalProgressText.textContent = 'Initializing...';
modalContainer.classList.remove("hidden");
setTimeout(() => {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
}
// A new function to update the progress bar and text
export function updateLoadingModal(percent, message) {
if (modalProgressBar && modalProgressText) {
const p = Math.max(0, Math.min(100, Math.round(percent))); // Clamp between 0-100
modalProgressBar.style.width = `${p}%`;
modalProgressText.textContent = message;
}
}
// The hideModal function now also resets the progress bar
export function hideModal(value) {
modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95");
setTimeout(() => {
modalContainer.classList.add("hidden");
if (modalProgressContainer && modalProgressBar && modalProgressText) {
modalProgressContainer.classList.add("hidden");
modalProgressBar.style.width = "0%";
modalProgressText.textContent = "";
}
if (modalResolve) modalResolve(value);
}, 200);
}
// Event listeners remain the same
modalOkBtn.addEventListener("click", () => hideModal(true));
modalCancelBtn.addEventListener("click", () => hideModal(false));
modalOverlay.addEventListener("click", () => hideModal(false));
Loading…
Cancel
Save