diff --git a/zoomsketch-issue/dom.js b/zoomsketch-issue/dom.js deleted file mode 100644 index 4bef76f..0000000 --- a/zoomsketch-issue/dom.js +++ /dev/null @@ -1,423 +0,0 @@ -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): ${driftMs.toFixed(0)}`); - 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: ${renderTime.toFixed(1)}ms`); - 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: ${videoRenderTime.toFixed(1)}ms`); - } else { - content.push("Load video and radar data to see sync info."); // Prompt to load data. - } - } - - debugOverlay.innerHTML = content.join("
"); // 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: ${colorMode} - | Drift: ${driftMs.toFixed( - 0 - )}ms - `; - } - - // --- 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); -}); diff --git a/zoomsketch-issue/main.js b/zoomsketch-issue/main.js deleted file mode 100644 index d4a8321..0000000 --- a/zoomsketch-issue/main.js +++ /dev/null @@ -1,1429 +0,0 @@ -// =========================================================================================================== -// REFACTOR PLAN: This monolithic script will be broken down into -// the following modules in the '/src' directory: -// -// - constants.js: Shared constants (VIDEO_FPS, RADAR_X_MAX) -// - utils.js: Pure helper functions (findRadarFrameIndexForTime) -// - state.js: Central application state management -// - dom.js: DOM element references and UI updaters -// - modal.js: Modal dialog logic -// - theme.js: Dark/Light mode theme switcher -// - db.js: IndexedDB caching logic -// - fileParsers.js: JSON and CAN log parsing logic -// - p5/radarSketch.js: The main p5.js radar visualization -// - p5/speedGraph.js: The p5.js speed graph visualization -// - sync.js: Playback and synchronization loop -// - main.js: The main application entry point that wires everything -// =========================================================================================================== - -import { zoomSketch } from "./p5/zoomSketch.js"; -import { - showModal, - hideModal, - updateLoadingModal, - showLoadingModal, -} from "./modal.js"; // Modify this import -import { animationLoop } from "./sync.js"; -import { radarSketch } from "./p5/radarSketch.js"; -import { speedGraphSketch } from "./p5/speedGraphSketch.js"; -import { parseVisualizationJson, parseJsonWithOboe } from "./fileParsers.js"; -import { - MAX_TRAJECTORY_LENGTH, - VIDEO_FPS, - RADAR_X_MIN, - RADAR_X_MAX, - RADAR_Y_MIN, - RADAR_Y_MAX, -} from "./constants.js"; -import { - findRadarFrameIndexForTime, - extractTimestampInfo, - parseTimestamp, - throttle, - formatTime, -} from "./utils.js"; -import { appState } from "./state.js"; -window.appState = appState; // exposing the appState to console -import { - themeToggleBtn, - canvasContainer, - canvasPlaceholder, - videoPlayer, - videoPlaceholder, - loadJsonBtn, - loadVideoBtn, - jsonFileInput, - videoFileInput, - playPauseBtn, - stopBtn, - timelineSlider, - frameCounter, - offsetInput, - speedSlider, - speedDisplay, - featureToggles, - toggleSnrColor, - toggleClusterColor, - toggleInlierColor, - toggleStationaryColor, - toggleVelocity, - toggleTracks, - toggleEgoSpeed, - toggleFrameNorm, - toggleDebugOverlay, - toggleDebug2Overlay, - egoSpeedDisplay, - debugOverlay, - snrMinInput, - snrMaxInput, - applySnrBtn, - autoOffsetIndicator, - clearCacheBtn, - speedGraphContainer, - speedGraphPlaceholder, - toggleCloseUp, - updateFrame, - resetVisualization, - updateDebugOverlay, - timelineTooltip, - saveSessionBtn, - loadSessionBtn, - sessionFileInput, - togglePredictedPos, - toggleCovariance, - updatePersistentOverlays, - collapsibleMenu, - toggleMenuBtn, - fullscreenBtn, - mainContent, - closeMenuBtn, - fullscreenEnterIcon, - fullscreenExitIcon, - menuScrim, - toggleConfirmedOnly, - resetUIForNewLoad, -} from "./dom.js"; - -import { initializeTheme } from "./theme.js"; - -import { initDB, saveFileWithMetadata, loadFreshFileFromDB } from "./db.js"; - -let seekDebounceTimer = null; //timeline slider variables. -let lastScrollTime = 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); -}); - -// PASTE THIS NEW, ENHANCED FUNCTION INTO main.js - -/** - * The core processing pipeline. This function orchestrates the entire - * loading, parsing, and initialization process in the correct order. - */ -async function processFilePipeline() { - // 1. Show the unified loading modal. - showLoadingModal("Starting file load..."); - - // 2. Handle JSON Parsing FIRST (if a JSON file is present) - if (jsonFileToLoad) { - // ... (The JSON loading part remains unchanged) ... - 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 }); - }); - - 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, now with a simulated progress spinner - if (videoFileToLoad) { - appState.videoFilename = videoFileToLoad.name; - localStorage.setItem("videoFilename", appState.videoFilename); - await saveFileWithMetadata("video", videoFileToLoad); - calculateAndSetOffset(); - - // --- START: NEW SIMULATED PROGRESS LOGIC --- - - // Start a simple text spinner animation in the modal - const spinnerChars = ["|", "/", "-", "\\"]; - let spinnerIndex = 0; - const spinnerInterval = setInterval(() => { - const spinnerText = spinnerChars[spinnerIndex % spinnerChars.length]; - updateLoadingModal(85, `Loading video ${spinnerText}`); // Keep progress bar mostly full - spinnerIndex++; - }, 150); // Update the spinner character every 150ms - - await new Promise((resolve, reject) => { - const onReady = () => { - clearInterval(spinnerInterval); // IMPORTANT: Stop the spinner - resolve(); - }; - const onError = (e) => { - clearInterval(spinnerInterval); // IMPORTANT: Stop the spinner on error - reject(e); - }; - videoPlayer.addEventListener('canplaythrough', onReady, { once: true }); - videoPlayer.addEventListener('error', onError, { once: true }); - setupVideoPlayer(URL.createObjectURL(videoFileToLoad)); - }); - // --- END: NEW SIMULATED PROGRESS LOGIC --- - } - - // 4. Finalize the setup - updateLoadingModal(95, "Finalizing visualization..."); - finalizeSetup(); - - setTimeout(() => { - updateLoadingModal(100, "Complete!"); - setTimeout(hideModal, 300); - }, 200); -} - -/* async function processFilePipeline() { - // 1. Show the unified loading modal. - showLoadingModal("Starting file load..."); - - // 2. Handle JSON Parsing (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"); - - // Use a promise to wait for the worker to finish parsing - const parsedData = await new Promise((resolve, reject) => { - worker.onmessage = (e) => { - const { type, data, percent, message } = e.data; - if (type === "progress") { - updateLoadingModal( - 15 + 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 }); - }); - - // 4. Post-process the parsed JSON data - updateLoadingModal(95, "Finalizing visualization..."); - 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 first (if a video file is present) - if (videoFileToLoad) { - updateLoadingModal(5, `Loading video: ${videoFileToLoad.name}`); - appState.videoFilename = videoFileToLoad.name; - localStorage.setItem("videoFilename", appState.videoFilename); - await saveFileWithMetadata("video", videoFileToLoad); - calculateAndSetOffset(); - - // Use a promise to wait until the video metadata is loaded - await new Promise((resolve, reject) => { - videoPlayer.onloadedmetadata = resolve; - videoPlayer.onerror = reject; - setupVideoPlayer(URL.createObjectURL(videoFileToLoad)); - }); - updateLoadingModal(15, "Video metadata loaded."); - } - // 5. Finalize the setup and hide the modal - updateLoadingModal(100, "Complete!"); - finalizeSetup(); - setTimeout(hideModal, 300); -} */ - -/** - * This function creates the p5 instances and finishes the UI setup. - * It's called after all file processing is complete. - */ -function finalizeSetup() { - // Make sure the canvas placeholder is hidden and toggles are visible - canvasPlaceholder.style.display = "none"; - featureToggles.classList.remove("hidden"); - - // 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 && videoPlayer.duration > 0) { - speedGraphPlaceholder.classList.add("hidden"); - if (!appState.speedGraphInstance) { - appState.speedGraphInstance = new p5(speedGraphSketch); - } - appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration); - } - - // Reset the visualization to the first frame - if (appState.vizData) { - snrMinInput.value = appState.globalMinSnr.toFixed(1); - snrMaxInput.value = appState.globalMaxSnr.toFixed(1); - resetVisualization(); - } -} - -// --- [END] CORRECTED UNIFIED FILE LOADING LOGIC --- - -// Sets up the video player with the given file URL. -function setupVideoPlayer(fileURL) { - videoPlayer.src = fileURL; - videoPlayer.classList.remove("hidden"); - videoPlaceholder.classList.add("hidden"); - videoPlayer.playbackRate = parseFloat(speedSlider.value); -} - -// In src/main.js, add this new function -function loadVideoWithProgress(videoObject) { - if (!videoObject) return; - - showModal("Loading video...", false, true); - updateModalProgress(0); - - // Define event handlers so we can add and remove them correctly - const onProgress = () => { - if (videoPlayer.duration > 0) { - // Find the end of the buffered content - const bufferedEnd = - videoPlayer.buffered.length > 0 ? videoPlayer.buffered.end(0) : 0; - const percent = (bufferedEnd / videoPlayer.duration) * 100; - updateModalProgress(percent); - } - }; - - const onCanPlayThrough = () => { - updateModalProgress(100); - // Give the user a moment to see 100% before closing the modal - setTimeout(() => { - document.getElementById("modal-ok-btn").click(); - }, 400); - - // Clean up the event listeners we added - videoPlayer.removeEventListener("progress", onProgress); - videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough); - }; - - const onError = () => { - showModal("Error: Could not load the video file."); - // Clean up event listeners on error - videoPlayer.removeEventListener("progress", onProgress); - videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough); - videoPlayer.removeEventListener("error", onError); - }; - - // This one-time event is for re-syncing data once the video's metadata is ready - - videoPlayer.addEventListener( - "loadedmetadata", - () => { - // This is the perfect time to re-sync data if needed - if (appState.vizData) { - console.log("DEBUG: Video metadata loaded. Re-calculating timestamps."); - appState.vizData.radarFrames.forEach((frame) => { - frame.timestampMs = - appState.radarStartTimeMs + - frame.timestamp - - appState.videoStartDate.getTime(); - }); - resetVisualization(); - } - - // --- START: New Speed Graph Logic --- - // If we have data and the video is ready, create/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: New Speed Graph Logic --- - }, - { once: true } - ); // { once: true } makes sure this runs only once per load - - // { once: true } //makes sure this runs only once per load - - // Add the listeners for progress tracking - videoPlayer.addEventListener("progress", onProgress); - videoPlayer.addEventListener("canplaythrough", onCanPlayThrough); - videoPlayer.addEventListener("error", onError); - - // Create the object URL and set the video source to trigger loading - const fileURL = URL.createObjectURL(videoObject); - setupVideoPlayer(fileURL); -} - -// Event listener for loading JSON file. -loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); -loadVideoBtn.addEventListener("click", () => videoFileInput.click()); - -clearCacheBtn.addEventListener("click", async () => { - const confirmed = await showModal("Clear all cached data and reload?", true); - if (confirmed) { - indexedDB.deleteDatabase("visualizerDB"); - localStorage.clear(); - window.location.reload(); - } -}); -// Event listener for saving the session -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, - jsonFilename: appState.jsonFilename, - videoFilename: appState.videoFilename, - offset: offsetInput.value, - playbackSpeed: speedSlider.value, - snrMin: snrMinInput.value, - snrMax: snrMaxInput.value, - 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); - - // --- Dynamic Filename Logic --- - const now = new Date(); - const pad = (num) => String(num).padStart(2, "0"); - const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad( - now.getDate() - )}`; - const time = `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad( - now.getSeconds() - )}`; - const timestamp = `${date}_${time}`; - const defaultFilename = `visualizer-session_${timestamp}.json`; - - // --- Trigger "Save As" Dialog --- - const a = document.createElement("a"); - a.href = url; - - // 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 clicking the link triggers the download/save dialog. - - document.body.removeChild(a); - 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) { - // 'now' is a high-resolution timestamp provided by the browser - if (appState.lastVideoFrameTime > 0) { - const delta = now - appState.lastVideoFrameTime; - appState.videoFrameRenderTime = delta; - } - appState.lastVideoFrameTime = now; - - // Re-register the callback for the next frame to create a loop - videoPlayer.requestVideoFrameCallback(videoFrameCallback); -} - -// 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 --- - -// --- Collapsible Menu Logic (Overlay Version) --- -function toggleMenu(show) { - if (show) { - collapsibleMenu.classList.remove("-translate-x-full"); - menuScrim.classList.remove("hidden"); // Show the scrim - // The line that pushed the content has been REMOVED. - } else { - collapsibleMenu.classList.add("-translate-x-full"); - menuScrim.classList.add("hidden"); // Hide the scrim - } -} - -toggleConfirmedOnly.addEventListener("change", () => { - if (appState.p5_instance) { - appState.p5_instance.redraw(); - } -}); - -// Open the menu -toggleMenuBtn.addEventListener("click", () => toggleMenu(true)); - -// Close the menu with the 'X' button -closeMenuBtn.addEventListener("click", () => toggleMenu(false)); - -// NEW: Close the menu by clicking on the scrim -menuScrim.addEventListener("click", () => toggleMenu(false)); - -// --- Fullscreen Logic --- -fullscreenBtn.addEventListener("click", () => { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen(); - } else if (document.exitFullscreen) { - document.exitFullscreen(); - } -}); - -// This listener updates the icon whenever fullscreen state changes, -// whether it's triggered by our button or the F11 key. -document.addEventListener("fullscreenchange", () => { - if (document.fullscreenElement) { - fullscreenEnterIcon.classList.add("hidden"); - fullscreenExitIcon.classList.remove("hidden"); - } else { - fullscreenEnterIcon.classList.remove("hidden"); - fullscreenExitIcon.classList.add("hidden"); - } -}); - -// jsonFileInput event listener -/* 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. -/* 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. -offsetInput.addEventListener("input", () => { - autoOffsetIndicator.classList.add("hidden"); - localStorage.setItem("visualizerOffset", offsetInput.value); -}); - -// Event listener for apply SNR button click. -applySnrBtn.addEventListener("click", () => { - const newMin = parseFloat(snrMinInput.value), - newMax = parseFloat(snrMaxInput.value); - if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) { - showModal("Invalid SNR range."); - return; - } - appState.globalMinSnr = newMin; - appState.globalMaxSnr = newMax; - toggleFrameNorm.checked = false; - if (appState.p5_instance) { - appState.p5_instance.drawSnrLegendToBuffer( - appState.globalMinSnr, - appState.globalMaxSnr - ); - appState.p5_instance.redraw(); - } -}); - -// Event listener for play/pause button click. -playPauseBtn.addEventListener("click", () => { - if (!appState.vizData && !videoPlayer.src) return; - appState.isPlaying = !appState.isPlaying; - playPauseBtn.textContent = appState.isPlaying ? "Pause" : "Play"; - if (appState.isPlaying) { - if (videoPlayer.src && videoPlayer.readyState > 1) { - appState.masterClockStart = performance.now(); - appState.mediaTimeStart = videoPlayer.currentTime; - appState.lastSyncTime = appState.masterClockStart; - videoPlayer.play(); - } - requestAnimationFrame(animationLoop); - } else { - if (videoPlayer.src) videoPlayer.pause(); - } -}); - -// Event listener for stop button click. -stopBtn.addEventListener("click", () => { - videoPlayer.pause(); - appState.isPlaying = false; - playPauseBtn.textContent = "Play"; - if (appState.vizData) { - updateFrame(0, true); - } else if (videoPlayer.src) { - videoPlayer.currentTime = 0; - } - if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); -}); - -// Event listener for timeline slider input. -// In src/main.js, REPLACE the existing timelineSlider 'input' event listener with this: - -timelineSlider.addEventListener("input", (event) => { - if (!appState.vizData) return; - updateDebugOverlay(videoPlayer.currentTime); - updatePersistentOverlays(videoPlayer.currentTime); - // --- 1. Live Seeking (Throttled for performance) --- - // This part gives you the immediate visual feedback as you drag the slider. - // We use a simple timestamp check to prevent it from running too often. - const now = performance.now(); - if ( - !timelineSlider.lastInputTime || - now - timelineSlider.lastInputTime > 32 - ) { - // ~30fps throttle - if (appState.isPlaying) { - videoPlayer.pause(); - appState.isPlaying = false; - playPauseBtn.textContent = "Play"; - } - const frame = parseInt(event.target.value, 10); - updateFrame(frame, true); - appState.mediaTimeStart = videoPlayer.currentTime; - appState.masterClockStart = now; - timelineSlider.lastInputTime = now; - } - - // --- 2. Final, Precise Sync (Debounced for reliability) --- - // This part ensures a perfect sync only AFTER you stop moving the slider. - clearTimeout(seekDebounceTimer); // Always cancel the previously scheduled sync - - seekDebounceTimer = setTimeout(() => { - console.log("Slider movement stopped. Performing final, debounced resync."); - const finalFrame = parseInt(event.target.value, 10); - updateFrame(finalFrame, true); // Perform the final, precise seek - - // Also update the debug overlay with the final, settled time - updateDebugOverlay(videoPlayer.currentTime); - }, 250); // Wait for 250ms of inactivity before firing -}); - -// --- Timeline Scroll-to-Seek Logic --- - -timelineSlider.addEventListener("wheel", (event) => { - if (!appState.vizData) return; - // 1. Prevent the page from scrolling up and down - event.preventDefault(); - - // 2. Calculate scroll speed - const now = performance.now(); - const timeDelta = now - (lastScrollTime || now); // Handle first scroll - lastScrollTime = now; - // Calculate speed as "events per second", giving more weight to recent, fast scrolls - scrollSpeed = timeDelta > 0 ? 1000 / timeDelta : scrollSpeed; - - // 3. Map scroll speed to a dynamic seek multiplier - // This creates a nice acceleration curve. The '50' is a sensitivity value you can adjust. - const speedMultiplier = 1 + Math.floor(scrollSpeed / 4); - const baseSeekAmount = 1; // Base frames to move on a slow scroll - let seekAmount = Math.max(baseSeekAmount, speedMultiplier); - - // 4. Calculate the new frame index - const direction = Math.sign(event.deltaY); // +1 for down/right, -1 for up/left - const currentFrame = parseInt(timelineSlider.value, 10); - let newFrame = currentFrame - seekAmount * direction; - - // Clamp the new frame to the valid range - const totalFrames = appState.vizData.radarFrames.length - 1; - newFrame = Math.max(0, Math.min(newFrame, totalFrames)); - - // 5. Update the UI - if (appState.isPlaying) { - playPauseBtn.click(); // Pause if playing - } - updateFrame(newFrame, true); - - // 6. Reuse the debouncer for a final, precise sync after scrolling stops - clearTimeout(seekDebounceTimer); - seekDebounceTimer = setTimeout(() => { - console.log("Scrolling stopped. Performing final, debounced resync."); - updateFrame(newFrame, true); - updatePersistentOverlays(videoPlayer.currentTime); - updateDebugOverlay(videoPlayer.currentTime); - }, 300); // Wait 300ms after the last scroll event -}); - -// In src/main.js, add this new block of event listeners -// --- Timeline Scrub-to-Seek Preview Logic --- - -timelineSlider.addEventListener("mouseover", () => { - if (appState.vizData) { - timelineTooltip.classList.remove("hidden"); - } -}); - -timelineSlider.addEventListener("mouseout", () => { - timelineTooltip.classList.add("hidden"); -}); - -timelineSlider.addEventListener("mousemove", (event) => { - if (!appState.vizData) return; - - // 1. Calculate the hover position as a fraction (0.0 to 1.0) - const rect = timelineSlider.getBoundingClientRect(); - const hoverFraction = (event.clientX - rect.left) / rect.width; - - // 2. Calculate the corresponding frame index - const sliderMax = - parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1; - let frameIndex = Math.round(hoverFraction * sliderMax); - // The value is already clamped by this calculation, but an extra check is safe - frameIndex = Math.max(0, Math.min(frameIndex, sliderMax)); - - const frameData = appState.vizData.radarFrames[frameIndex]; - if (!frameData) return; - - // 3. Update the tooltip's content - const formattedTime = formatTime(frameData.timestampMs); - timelineTooltip.innerHTML = `Frame: ${ - frameIndex + 1 - }
Time: ${formattedTime}`; - - // 4. Position the tooltip horizontally above the cursor - // The horizontal position is the mouse's X relative to the slider's start - const tooltipX = event.clientX - rect.left; - timelineTooltip.style.left = `${tooltipX}px`; -}); - -// Event listener for speed slider input. -speedSlider.addEventListener("input", (event) => { - const speed = parseFloat(event.target.value); - videoPlayer.playbackRate = speed; - speedDisplay.textContent = `${speed.toFixed(1)}x`; -}); - -const colorToggles = [ - toggleSnrColor, - toggleClusterColor, - toggleInlierColor, - toggleStationaryColor, -]; -colorToggles.forEach((t) => { - t.addEventListener("change", (e) => { - if (e.target.checked) { - colorToggles.forEach((o) => { - if (o !== e.target) o.checked = false; - }); - } - if (appState.p5_instance) appState.p5_instance.redraw(); - updatePersistentOverlays(videoPlayer.currentTime); - }); -}); - -[ - toggleVelocity, - toggleEgoSpeed, - toggleFrameNorm, - toggleTracks, - toggleDebugOverlay, - toggleDebug2Overlay, -].forEach((t) => { - t.addEventListener("change", () => { - if (appState.p5_instance) { - if (t === toggleFrameNorm && !toggleFrameNorm.checked) - appState.p5_instance.drawSnrLegendToBuffer( - appState.globalMinSnr, - appState.globalMaxSnr - ); - appState.p5_instance.redraw(); - } - if (t === toggleDebugOverlay || t === toggleDebug2Overlay) { - updateDebugOverlay(videoPlayer.currentTime); - updatePersistentOverlays(videoPlayer.currentTime); - } - }); -}); - -toggleCloseUp.addEventListener("change", () => { - appState.isCloseUpMode = toggleCloseUp.checked; - if (appState.p5_instance) { - if (appState.isCloseUpMode) { - if (appState.isPlaying) { - playPauseBtn.click(); - } - appState.p5_instance.loop(); - } else { - appState.p5_instance.noLoop(); - appState.p5_instance.redraw(); - } - } -}); - -videoPlayer.addEventListener("ended", () => { - appState.isPlaying = false; - playPauseBtn.textContent = "Play"; -}); - -document.addEventListener("keydown", (event) => { - // --- FIX APPLIED HERE --- - // We only want to block shortcuts if the user is actively typing in a text or number input. - // This allows shortcuts to work even when other elements, like the timeline slider, are focused. - const isTextInputFocused = - event.target.tagName === "INPUT" && - (event.target.type === "text" || event.target.type === "number"); - if (isTextInputFocused) { - return; - } - // --- END OF FIX --- - - const key = event.key; - // We can add any new shortcut keys to this array. - const recognizedKeys = [ - "ArrowRight", - "ArrowLeft", - " ", - "1", - "2", - "3", - "4", - "t", - "d", - "g", - "r", - "p", - "a", - "s", - "m", - "q", - "c", - ]; - - if (!appState.vizData || !recognizedKeys.includes(key)) { - return; - } - - event.preventDefault(); - - // --- Spacebar for Play/Pause --- - if (key === " ") { - playPauseBtn.click(); - } - - // --- Arrow keys for frame-by-frame seeking --- - if (key === "ArrowRight" || key === "ArrowLeft") { - if (appState.isPlaying) { - playPauseBtn.click(); - } - let newFrame = appState.currentFrame; - if (key === "ArrowRight") { - newFrame = Math.min( - appState.vizData.radarFrames.length - 1, - appState.currentFrame + 1 - ); - } else if (key === "ArrowLeft") { - newFrame = Math.max(0, appState.currentFrame - 1); - } - if (newFrame !== appState.currentFrame) { - updateFrame(newFrame, true); - } - } - - // --- Number keys for color modes --- - if (key >= "1" && key <= "4") { - const colorToggles = [ - toggleSnrColor, - toggleClusterColor, - toggleInlierColor, - toggleStationaryColor, - ]; - const toggleIndex = parseInt(key) - 1; - if (colorToggles[toggleIndex]) { - colorToggles[toggleIndex].click(); - } - } - if (key === "q") { - themeToggleBtn.click(); - } - if (key === "t") { - toggleTracks.click(); - } - if (key === "d") { - toggleVelocity.click(); - } - if (key === "g") { - toggleCloseUp.click(); - } - if (key === "r") { - resetVisualization(); - } - if (key === "c") { - appState.isRawOnlyMode = !appState.isRawOnlyMode; - if (appState.p5_instance) { - appState.p5_instance.redraw(); - } - } - - if (key === "p") { - togglePredictedPos.click(); - appState.p5_instance.redraw(); - } - if (key === "s") { - toggleSnrColor.click(); - } - if (key === "a") { - toggleDebugOverlay.click(); - toggleDebug2Overlay.click(); - if (isDebug1Visible && isDebug2Visible) { - radarInfoOverlay.classList.add("hidden"); - videoInfoOverlay.classList.add("hidden"); - return; - } - // Otherwise, make sure they are visible. - radarInfoOverlay.classList.remove("hidden"); - videoInfoOverlay.classList.remove("hidden"); - } - if (key === "m") { - if (collapsibleMenu.classList.contains("-translate-x-full")) { - // If the menu is hidden (closed), trigger a click on the OPEN button. - toggleMenuBtn.click(); - } else { - // If the menu is not hidden (it's open), trigger a click on the CLOSE button. - closeMenuBtn.click(); - } - } -}); - -function calculateAndSetOffset() { - const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); - const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); - if (videoTimestampInfo) { - appState.videoStartDate = parseTimestamp( - videoTimestampInfo.timestampStr, - videoTimestampInfo.format - ); - if (appState.videoStartDate) - console.log( - `Video start date set to: ${appState.videoStartDate.toISOString()}` - ); - } - if (jsonTimestampInfo) { - const jsonDate = parseTimestamp( - jsonTimestampInfo.timestampStr, - jsonTimestampInfo.format - ); - if (jsonDate) { - appState.radarStartTimeMs = jsonDate.getTime(); - console.log(`Radar start date set to: ${jsonDate.toISOString()}`); - if (appState.videoStartDate) { - const offset = - appState.radarStartTimeMs - appState.videoStartDate.getTime(); - offsetInput.value = offset; - localStorage.setItem("visualizerOffset", offset); - autoOffsetIndicator.classList.remove("hidden"); - console.log(`Auto-calculated offset: ${offset} ms`); - } - } - } -} - -// Application Initialization - -// FILE: steps/src/main.js - -// REPLACE the entire 'document.addEventListener("DOMContentLoaded", ...)' block with this: -/* document.addEventListener("DOMContentLoaded", () => { - initializeTheme(); - console.log("DEBUG: DOMContentLoaded fired. Starting session load."); - - 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); - } - - // 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(); - - const videoBlob = await loadFreshFileFromDB( - "video", - appState.videoFilename - ); - const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename); - - console.log( - "DEBUG: Freshness checks complete. Proceeding with valid data." - ); - - const finalizeSetup = async (parsedJson) => { - if (parsedJson) { - const result = await parseVisualizationJson( - parsedJson, - appState.radarStartTimeMs, - appState.videoStartDate - ); - - 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 { - await finalizeSetup(null); - loadVideoWithProgress(videoBlob); - } - }); -}); */ - -/* // In main.js - -// --- INITIALIZATION --- -document.addEventListener("DOMContentLoaded", () => { - initializeTheme(); - initDB(async () => { - console.log("Database initialized. Checking for cached session..."); - - // --- START: RESTORED AUTO-RELOAD LOGIC --- - // Get the filenames we expect to find in the cache - appState.jsonFilename = localStorage.getItem("jsonFilename"); - appState.videoFilename = localStorage.getItem("videoFilename"); - - if (appState.jsonFilename) { - const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename); - const videoBlob = await loadFreshFileFromDB("video", appState.videoFilename); - - // If the required JSON file is in the cache, start the loading process - if (jsonBlob) { - console.log("Cached session found. Starting auto-reload..."); - filesToLoad = { json: jsonBlob, video: videoBlob }; - startLoadingProcess(); // This will show our new unified modal - processJsonFile(jsonBlob); // Start the process with the cached file - } else { - console.log("No valid cached session found. Ready for manual file load."); - } - } else { - console.log("No previous session found. Ready for manual file load."); - } - // --- END: RESTORED AUTO-RELOAD LOGIC --- - }); -}); */ - -// --- [START] CORRECTED INITIALIZATION LOGIC --- -document.addEventListener("DOMContentLoaded", () => { - initializeTheme(); - initDB(async () => { - console.log("Database initialized. Checking for cached session..."); - - appState.jsonFilename = localStorage.getItem("jsonFilename"); - appState.videoFilename = localStorage.getItem("videoFilename"); - - if (appState.jsonFilename) { - const jsonBlob = await loadFreshFileFromDB("json", appState.jsonFilename); - const videoBlob = await loadFreshFileFromDB( - "video", - appState.videoFilename - ); - - 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." - ); - } - } else { - console.log("No previous session found. Ready for manual file load."); - } - }); -}); -// --- [END] CORRECTED INITIALIZATION LOGIC --- - -// In src/main.js, add this new event listener -offsetInput.addEventListener("keydown", (event) => { - // Check if the key pressed was 'Enter' - if (event.key === "Enter") { - // Prevent the default browser action for the Enter key (like submitting a form) - event.preventDefault(); - - // Make sure visualization data is loaded before proceeding - if (!appState.vizData) return; - - console.log( - `Enter pressed. Forcing resync with new offset: ${offsetInput.value}` - ); - - // If the video is playing, pause it to allow for precise frame tuning. - if (appState.isPlaying) { - playPauseBtn.click(); - } - - // Call updateFrame, forcing it to resync the video to the current radar frame - // using the new offset value from the input box. - updateFrame(appState.currentFrame, true); - } -}); diff --git a/zoomsketch-issue/modal.js b/zoomsketch-issue/modal.js deleted file mode 100644 index e8a8c16..0000000 --- a/zoomsketch-issue/modal.js +++ /dev/null @@ -1,97 +0,0 @@ -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)); \ No newline at end of file