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