9 changed files with 6 additions and 781 deletions
-
22steps/src/dataExplorer.js
-
13steps/src/db.js
-
55steps/src/drawUtils.js
-
11steps/src/fileParsers.js
-
28steps/src/main.js
-
617steps/src/main_old.js
-
17steps/src/p5/radarSketch.js
-
6steps/src/sync.js
-
18steps/src/utils.js
@ -1,617 +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 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); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue