Browse Source
1.) Radar sketch distance increased to 80m in Y axis.
1.) Radar sketch distance increased to 80m in Y axis.
2.) feat(visualization): Add advanced overlays and robust large-file streaming This commit introduces several major features and critical bug fixes to the visualizer, significantly enhancing its analytical capabilities and performance. The primary focus was on adding more detailed visualization overlays from the Kalman filter and implementing a robust solution for handling very large JSON files that were previously crashing the application. ### New Features - **Covariance Ellipse Overlay:** - A "Show Covariance" checkbox has been added to the UI. - When enabled, the visualizer now draws the 95% confidence ellipse for each track's predicted position, derived from the `covarianceP` matrix. This provides a real-time view of the Kalman filter's positional uncertainty. - **Predicted vs. Corrected Position Markers:** - A "Show Predicted Position" checkbox has been added. - This feature displays the filter's raw prediction (red cross) alongside the final corrected position (blue cross), making it easy to visualize the predict-correct cycle and analyze the filter's behavior during object acceleration or maneuvers. ### Bug Fixes & Performance Enhancements - **Fix: Marker for Lost Tracks:** - The main track marker (blue cross) now correctly disappears if its `correctedPosition` is null for a given frame. This provides a clear and intuitive visual cue that a track has been temporarily "lost" and is coasting on predictions alone. - **Fix: Large JSON File Parsing (Streaming & Web Worker):** - Resolved a critical "Maximum call stack size exceeded" error that occurred when loading JSON files larger than ~30MB. - The entire file loading and parsing pipeline has been refactored to use a Web Worker. This moves the CPU-intensive parsing off the main UI thread, preventing the page from freezing. - The worker streams the file and uses a lightweight parser (Clarinet.js) to build the data object, ensuring low memory usage and a responsive interface. - **feat: Add Progress Bar for File Loading:** - To provide feedback during the new streaming process, a progress bar is now displayed in the modal, showing the real-time progress of the file parsing operation.refactor/modularize
6 changed files with 900 additions and 138 deletions
-
14steps/index.html
-
2steps/src/constants.js
-
131steps/src/fileParsers.js
-
168steps/src/main.js
-
617steps/src/main_old.js
-
106steps/src/parser.worker.js
@ -0,0 +1,617 @@ |
|||||
|
// ===========================================================================================================
|
||||
|
// 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 animation loop from './src/sync.js';
|
||||
|
|
||||
|
import { animationLoop } from "./sync.js"; |
||||
|
// import radar sketch from './src/p5/radarSketch.js';
|
||||
|
import { radarSketch } from "./p5/radarSketch.js"; |
||||
|
// import speed graph sketch from './src/p5/speedGraphSketch.js';
|
||||
|
import { speedGraphSketch } from "./p5/speedGraphSketch.js"; |
||||
|
// import JSON parser, can log procesor from './src/fileParsers.js';
|
||||
|
import { processCanLog, parseVisualizationJson } from "./fileParsers.js"; |
||||
|
// import constants from './constants.js';
|
||||
|
import { |
||||
|
MAX_TRAJECTORY_LENGTH, |
||||
|
VIDEO_FPS, |
||||
|
RADAR_X_MIN, |
||||
|
RADAR_X_MAX, |
||||
|
RADAR_Y_MIN, |
||||
|
RADAR_Y_MAX, |
||||
|
} from "./constants.js"; |
||||
|
// import utils and helpers from './src/utils.js';
|
||||
|
import { |
||||
|
findRadarFrameIndexForTime, |
||||
|
findLastCanIndexBefore, |
||||
|
extractTimestampInfo, |
||||
|
parseTimestamp, |
||||
|
throttle, |
||||
|
} from "./utils.js"; |
||||
|
// import state machine from './src/state.js';
|
||||
|
import { appState } from "./state.js"; |
||||
|
// import DOM elements and UI updaters from './src/dom.js';
|
||||
|
import { |
||||
|
//---DOM Elements---//
|
||||
|
canvasContainer, |
||||
|
canvasPlaceholder, |
||||
|
videoPlayer, |
||||
|
videoPlaceholder, |
||||
|
loadJsonBtn, |
||||
|
loadVideoBtn, |
||||
|
loadCanBtn, |
||||
|
jsonFileInput, |
||||
|
videoFileInput, |
||||
|
canFileInput, |
||||
|
playPauseBtn, |
||||
|
stopBtn, |
||||
|
timelineSlider, |
||||
|
frameCounter, |
||||
|
offsetInput, |
||||
|
speedSlider, |
||||
|
speedDisplay, |
||||
|
featureToggles, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
toggleVelocity, |
||||
|
toggleTracks, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
egoSpeedDisplay, |
||||
|
canSpeedDisplay, |
||||
|
debugOverlay, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
applySnrBtn, |
||||
|
autoOffsetIndicator, |
||||
|
clearCacheBtn, |
||||
|
speedGraphContainer, |
||||
|
speedGraphPlaceholder, |
||||
|
toggleCloseUp, |
||||
|
//---UI Updaters---//
|
||||
|
updateFrame, |
||||
|
resetVisualization, |
||||
|
updateCanDisplay, |
||||
|
updateDebugOverlay, |
||||
|
} from "./dom.js"; |
||||
|
// Import modal dialog logic from './src/modal.js'.
|
||||
|
import { showModal } from "./modal.js"; |
||||
|
// Import theme initialization from './src/theme.js'.
|
||||
|
import { initializeTheme } from "./theme.js"; |
||||
|
// Import caching logic from './src/db.js'.
|
||||
|
import { initDB, saveFileToDB, loadFileFromDB } from "./db.js"; |
||||
|
|
||||
|
// 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); |
||||
|
} |
||||
|
// Event listener for loading JSON file.
|
||||
|
loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); |
||||
|
loadVideoBtn.addEventListener("click", () => videoFileInput.click()); |
||||
|
loadCanBtn.addEventListener("click", () => canFileInput.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 JSON file input change.
|
||||
|
jsonFileInput.addEventListener("change", (event) => { |
||||
|
const file = event.target.files[0]; |
||||
|
if (!file) return; |
||||
|
appState.jsonFilename = file.name; |
||||
|
localStorage.setItem("jsonFilename", appState.jsonFilename); |
||||
|
calculateAndSetOffset(); // This function now correctly sets appState variables
|
||||
|
|
||||
|
// Show a modal or loading indicator to the user
|
||||
|
showModal("Parsing large JSON file, please wait..."); |
||||
|
|
||||
|
// Get a readable stream from the file
|
||||
|
const stream = file.stream(); |
||||
|
|
||||
|
// Call the new streaming parser
|
||||
|
parseJsonStream(stream, (parsedData) => { |
||||
|
// This callback runs once the entire file has been parsed
|
||||
|
|
||||
|
// Once parsing is complete, continue with the rest of the setup
|
||||
|
const result = parseVisualizationJson( |
||||
|
parsedData, // We now pass the parsed object, not a string
|
||||
|
appState.radarStartTimeMs, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
|
||||
|
if (result.error) { |
||||
|
showModal(result.error); |
||||
|
return; |
||||
|
} |
||||
|
appState.vizData = result.data; |
||||
|
appState.globalMinSnr = result.minSnr; |
||||
|
appState.globalMaxSnr = result.maxSnr; |
||||
|
|
||||
|
// Update UI
|
||||
|
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); |
||||
|
} |
||||
|
// Hide the loading modal
|
||||
|
// Note: You might need to adjust your hideModal logic if it's not already globally accessible
|
||||
|
document.getElementById("modal-ok-btn").click(); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
const jsonString = e.target.result; |
||||
|
saveFileToDB("json", jsonString); |
||||
|
|
||||
|
// 1. Give the raw ingredients to our new JSON "chef"
|
||||
|
const result = parseVisualizationJson( |
||||
|
jsonString, |
||||
|
appState.radarStartTimeMs, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
|
||||
|
// 2. Check the result
|
||||
|
if (result.error) { |
||||
|
showModal(result.error); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 3. Update the application's central state with the prepared data
|
||||
|
appState.vizData = result.data; |
||||
|
appState.globalMinSnr = result.minSnr; |
||||
|
appState.globalMaxSnr = result.maxSnr; |
||||
|
|
||||
|
// 4. Now, the "waiter" updates the UI
|
||||
|
snrMinInput.value = appState.globalMinSnr.toFixed(1); |
||||
|
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); |
||||
|
resetVisualization(); // This UI function is in dom.js
|
||||
|
canvasPlaceholder.style.display = "none"; |
||||
|
featureToggles.classList.remove("hidden"); |
||||
|
|
||||
|
if (!appState.p5_instance) { |
||||
|
appState.p5_instance = new p5(radarSketch); |
||||
|
} |
||||
|
|
||||
|
if (appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} else { |
||||
|
// Redraw p5 instance with new data
|
||||
|
appState.p5_instance.drawSnrLegendToBuffer( |
||||
|
appState.globalMinSnr, |
||||
|
appState.globalMaxSnr |
||||
|
); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(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); |
||||
|
saveFileToDB("video", file); |
||||
|
|
||||
|
// This is the key moment: we now have a video start date.
|
||||
|
calculateAndSetOffset(); |
||||
|
|
||||
|
// Now, check if we have pending data that needs this date.
|
||||
|
if (appState.rawCanLogText) { |
||||
|
const result = processCanLog( |
||||
|
appState.rawCanLogText, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
if (!result.error) { |
||||
|
appState.canData = result.data; |
||||
|
appState.rawCanLogText = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// NEW: Re-process vizData if it was loaded before the video.
|
||||
|
if (appState.vizData) { |
||||
|
console.log("DEBUG: Video loaded after JSON. Re-calculating timestamps."); |
||||
|
appState.vizData.radarFrames.forEach((frame) => { |
||||
|
frame.timestampMs = |
||||
|
appState.radarStartTimeMs + |
||||
|
frame.timestamp - |
||||
|
appState.videoStartDate.getTime(); |
||||
|
}); |
||||
|
resetVisualization(); // Reset UI to reflect new timestamps
|
||||
|
} |
||||
|
|
||||
|
const fileURL = URL.createObjectURL(file); |
||||
|
setupVideoPlayer(fileURL); |
||||
|
|
||||
|
// When the video is ready, update the speed graph
|
||||
|
videoPlayer.onloadedmetadata = () => { |
||||
|
if (appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
}); |
||||
|
// Event listener for CAN file input change.
|
||||
|
canFileInput.addEventListener("change", (event) => { |
||||
|
const file = event.target.files[0]; |
||||
|
if (!file) return; |
||||
|
appState.canLogFilename = file.name; |
||||
|
localStorage.setItem("canLogFilename", appState.canLogFilename); |
||||
|
|
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
const logContent = e.target.result; |
||||
|
saveFileToDB("canLogText", logContent); |
||||
|
|
||||
|
// 1. Give the raw ingredients to the chef (our parser)
|
||||
|
const result = processCanLog(logContent, appState.videoStartDate); |
||||
|
|
||||
|
// 2. Check what the chef gave back
|
||||
|
if (result.error) { |
||||
|
// If there was an error, show it and save the raw text for later.
|
||||
|
showModal(result.error); |
||||
|
appState.rawCanLogText = result.rawCanLogText; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 3. If successful, update the application's central state
|
||||
|
appState.canData = result.data; |
||||
|
appState.rawCanLogText = null; |
||||
|
|
||||
|
// 4. Now, the waiter updates the UI based on the new state
|
||||
|
if (appState.canData.length > 0 || appState.vizData) { |
||||
|
speedGraphPlaceholder.classList.add("hidden"); |
||||
|
if (!appState.speedGraphInstance) { |
||||
|
// We need to pass the speedGraphSketch function definition here
|
||||
|
appState.speedGraphInstance = new p5(speedGraphSketch); |
||||
|
} |
||||
|
if (videoPlayer.duration) { |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} |
||||
|
} else { |
||||
|
showModal(`No CAN messages with ID 0x30F found.`); |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(file); |
||||
|
}); |
||||
|
// 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.
|
||||
|
timelineSlider.addEventListener( |
||||
|
"input", |
||||
|
throttle((event) => { |
||||
|
if (!appState.vizData) return; |
||||
|
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 = performance.now(); |
||||
|
}, 16) |
||||
|
); // Throttle delay for smoother updates.
|
||||
|
// Currently set at 16 ms to achieve smooth 60fps.
|
||||
|
// 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`; |
||||
|
}); |
||||
|
|
||||
|
// ADD THE NEW TOGGLE TO THE ARRAY
|
||||
|
// Array of color toggles.
|
||||
|
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(); |
||||
|
}); |
||||
|
}); |
||||
|
// Event listeners for various feature toggles.
|
||||
|
[ |
||||
|
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); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
// Event listener for close-up toggle.
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
// Event listener for video ended event.
|
||||
|
videoPlayer.addEventListener("ended", () => { |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
}); |
||||
|
// Event listener for keyboard arrow key presses to navigate frames.
|
||||
|
document.addEventListener("keydown", (event) => { |
||||
|
if ( |
||||
|
!appState.vizData || |
||||
|
["ArrowRight", "ArrowLeft"].indexOf(event.key) === -1 |
||||
|
) |
||||
|
return; |
||||
|
event.preventDefault(); |
||||
|
if (appState.isPlaying) { |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
videoPlayer.pause(); |
||||
|
} |
||||
|
let newFrame = appState.currentFrame; |
||||
|
if (event.key === "ArrowRight") |
||||
|
newFrame = Math.min( |
||||
|
appState.vizData.radarFrames.length - 1, |
||||
|
appState.currentFrame + 1 |
||||
|
); |
||||
|
else if (event.key === "ArrowLeft") |
||||
|
newFrame = Math.max(0, appState.currentFrame - 1); |
||||
|
if (newFrame !== appState.currentFrame) { |
||||
|
updateFrame(newFrame, true); |
||||
|
appState.mediaTimeStart = videoPlayer.currentTime; |
||||
|
appState.masterClockStart = performance.now(); |
||||
|
} |
||||
|
}); |
||||
|
// Calculates and sets the time offset between JSON and video timestamps.
|
||||
|
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: Event listener for DOMContentLoaded.
|
||||
|
document.addEventListener("DOMContentLoaded", () => { |
||||
|
initializeTheme(); |
||||
|
console.log("DEBUG: DOMContentLoaded fired. Starting session load."); // Log for debugging.
|
||||
|
|
||||
|
initDB(() => { |
||||
|
console.log("DEBUG: Database initialized."); |
||||
|
const savedOffset = localStorage.getItem("visualizerOffset"); |
||||
|
if (savedOffset !== null) { |
||||
|
offsetInput.value = savedOffset; |
||||
|
} |
||||
|
appState.videoFilename = localStorage.getItem("videoFilename"); |
||||
|
appState.jsonFilename = localStorage.getItem("jsonFilename"); |
||||
|
appState.canLogFilename = localStorage.getItem("canLogFilename"); |
||||
|
|
||||
|
// This is important: it sets videoStartDate if a video filename is cached
|
||||
|
calculateAndSetOffset(); // Calculate offset based on cached filenames.
|
||||
|
|
||||
|
// Promises to load files from IndexedDB.
|
||||
|
const videoPromise = new Promise((resolve) => |
||||
|
loadFileFromDB("video", resolve) |
||||
|
); |
||||
|
const jsonPromise = new Promise((resolve) => |
||||
|
loadFileFromDB("json", resolve) |
||||
|
); |
||||
|
const canLogPromise = new Promise((resolve) => |
||||
|
loadFileFromDB("canLogText", resolve) |
||||
|
); |
||||
|
// Once all files are loaded from DB, process them.
|
||||
|
Promise.all([videoPromise, jsonPromise, canLogPromise]) |
||||
|
.then(([videoBlob, jsonString, canLogText]) => { |
||||
|
console.log("DEBUG: All data fetched from IndexedDB."); |
||||
|
|
||||
|
const processAllData = () => { |
||||
|
console.log("DEBUG: Processing all loaded data."); |
||||
|
|
||||
|
// 1. Process JSON (only if video start date is available).
|
||||
|
if (jsonString && appState.videoStartDate) { |
||||
|
const result = parseVisualizationJson( |
||||
|
jsonString, |
||||
|
appState.radarStartTimeMs, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
if (!result.error) { |
||||
|
appState.vizData = result.data; |
||||
|
appState.globalMinSnr = result.minSnr; |
||||
|
appState.globalMaxSnr = result.maxSnr; |
||||
|
snrMinInput.value = appState.globalMinSnr.toFixed(1); |
||||
|
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); |
||||
|
} else { |
||||
|
showModal(result.error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. Process CAN log (only if video start date is available).
|
||||
|
if (canLogText && appState.videoStartDate) { |
||||
|
const result = processCanLog(canLogText, appState.videoStartDate); |
||||
|
if (!result.error) { |
||||
|
appState.canData = result.data; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 3. Update all UI elements now that data is processed.
|
||||
|
if (appState.vizData) { |
||||
|
resetVisualization(); |
||||
|
canvasPlaceholder.style.display = "none"; |
||||
|
featureToggles.classList.remove("hidden"); |
||||
|
if (!appState.p5_instance) { |
||||
|
appState.p5_instance = new p5(radarSketch); |
||||
|
} |
||||
|
} |
||||
|
if (appState.canData.length > 0 || appState.vizData) { |
||||
|
speedGraphPlaceholder.classList.add("hidden"); |
||||
|
if (!appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance = new p5(speedGraphSketch); |
||||
|
} |
||||
|
appState.speedGraphInstance.setData( |
||||
|
appState.canData, |
||||
|
appState.vizData, |
||||
|
videoPlayer.duration |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Main controller for processing data based on video availability.
|
||||
|
if (videoBlob) { |
||||
|
const fileURL = URL.createObjectURL(videoBlob); |
||||
|
setupVideoPlayer(fileURL); |
||||
|
// This ensures we ONLY process data once the video's duration is known.
|
||||
|
videoPlayer.onloadedmetadata = processAllData; |
||||
|
} else { |
||||
|
// If there's no video, process other data immediately.
|
||||
|
processAllData(); |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error("DEBUG: Error during Promise.all data loading:", error); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,106 @@ |
|||||
|
// Import the lightweight and worker-safe Clarinet library
|
||||
|
importScripts('https://cdn.jsdelivr.net/npm/clarinet@0.12.5/clarinet.min.js'); |
||||
|
|
||||
|
self.onmessage = async function(event) { |
||||
|
const file = event.data.file; |
||||
|
if (!file) { |
||||
|
self.postMessage({ type: 'error', message: 'No file received in worker.' }); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
console.log('Worker: Starting robust parsing with debugging...'); |
||||
|
|
||||
|
const fileSize = file.size; |
||||
|
let bytesRead = 0; |
||||
|
let lastReportedProgress = -1; |
||||
|
|
||||
|
const parser = clarinet.parser(); |
||||
|
const vizData = { radarFrames: [], tracks: [] }; |
||||
|
|
||||
|
// A simple state machine to track our location
|
||||
|
let state = { |
||||
|
inRadarFrames: false, |
||||
|
inTracks: false, |
||||
|
currentObject: null, |
||||
|
currentKey: '' |
||||
|
}; |
||||
|
|
||||
|
parser.onkey = (key) => { |
||||
|
state.currentKey = key; |
||||
|
if (key === 'radarFrames') state.inRadarFrames = true; |
||||
|
if (key === 'tracks') state.inTracks = true; |
||||
|
}; |
||||
|
|
||||
|
parser.onopenobject = () => { |
||||
|
// We only care about objects inside our target arrays
|
||||
|
if (state.inRadarFrames || state.inTracks) { |
||||
|
state.currentObject = {}; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
parser.oncloseobject = () => { |
||||
|
if (state.currentObject) { |
||||
|
if (state.inRadarFrames) { |
||||
|
vizData.radarFrames.push(state.currentObject); |
||||
|
} else if (state.inTracks) { |
||||
|
vizData.tracks.push(state.currentObject); |
||||
|
} |
||||
|
state.currentObject = null; // Reset for the next object
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
parser.onclosearray = () => { |
||||
|
// When we finish an array, update our state
|
||||
|
if (state.inRadarFrames) state.inRadarFrames = false; |
||||
|
if (state.inTracks) state.inTracks = false; |
||||
|
}; |
||||
|
|
||||
|
parser.onvalue = (value) => { |
||||
|
if (state.currentObject && state.currentKey) { |
||||
|
state.currentObject[state.currentKey] = value; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
parser.onend = () => { |
||||
|
// --- DEBUGGING MESSAGES ---
|
||||
|
console.log("Worker: Parsing complete."); |
||||
|
console.log("Worker: Final vizData structure:", vizData); |
||||
|
console.log("Worker: Number of radar frames parsed:", vizData.radarFrames ? vizData.radarFrames.length : 'undefined'); |
||||
|
console.log("Worker: Number of tracks parsed:", vizData.tracks ? vizData.tracks.length : 'undefined'); |
||||
|
// --- END DEBUGGING ---
|
||||
|
|
||||
|
self.postMessage({ type: 'progress', percent: 100 }); |
||||
|
self.postMessage({ type: 'complete', data: vizData }); |
||||
|
}; |
||||
|
|
||||
|
parser.onerror = (err) => { |
||||
|
console.error("Worker: Clarinet parsing error:", err); |
||||
|
self.postMessage({ type: 'error', message: 'Failed to parse JSON structure.' }); |
||||
|
}; |
||||
|
|
||||
|
// --- Stream Reading Logic (remains the same) ---
|
||||
|
const stream = file.stream(); |
||||
|
const reader = stream.getReader(); |
||||
|
const decoder = new TextDecoder(); |
||||
|
|
||||
|
while (true) { |
||||
|
const { done, value } = await reader.read(); |
||||
|
if (done) { |
||||
|
parser.close(); |
||||
|
break; |
||||
|
} |
||||
|
bytesRead += value.length; |
||||
|
const percent = Math.round((bytesRead / fileSize) * 100); |
||||
|
if (percent > lastReportedProgress) { |
||||
|
self.postMessage({ type: 'progress', percent: percent }); |
||||
|
lastReportedProgress = percent; |
||||
|
} |
||||
|
parser.write(decoder.decode(value, { stream: true })); |
||||
|
} |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error("Worker: An error occurred during streaming:", error); |
||||
|
self.postMessage({ type: 'error', message: 'Failed to read file in worker.' }); |
||||
|
} |
||||
|
}; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue