Browse Source

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
RUSHIL AMBARISH KADU 9 months ago
parent
commit
e7d8671e5a
  1. 14
      steps/index.html
  2. 2
      steps/src/constants.js
  3. 113
      steps/src/fileParsers.js
  4. 140
      steps/src/main.js
  5. 617
      steps/src/main_old.js
  6. 106
      steps/src/parser.worker.js

14
steps/index.html

@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Radar and Video Visualizer - Timestamp Synchronized</title> <title>Radar and Video Visualizer - Timestamp Synchronized</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/oboe@2.1.5/dist/oboe-browser.min.js"></script>
<!-- DARK MODE: Step 1 - Configure Tailwind to use the 'class' strategy for dark mode --> <!-- DARK MODE: Step 1 - Configure Tailwind to use the 'class' strategy for dark mode -->
<script> <script>
tailwind.config = { tailwind.config = {
@ -71,6 +73,18 @@
#modal-content { #modal-content {
transition: transform 0.2s ease-in-out; transition: transform 0.2s ease-in-out;
} }
/* ... (your existing styles) ... */
#modal-content {
transition: transform 0.2s ease-in-out;
}
/* ADD THIS NEW CSS RULE */
#progress-bar {
transition: width 0.1s linear;
}
</style> </style>
</head> </head>

2
steps/src/constants.js

@ -9,4 +9,4 @@ export const RADAR_X_MAX = 20;
// Minimum Y-coordinate for the radar plot in meters. // Minimum Y-coordinate for the radar plot in meters.
export const RADAR_Y_MIN = 0; export const RADAR_Y_MIN = 0;
// Maximum Y-coordinate for the radar plot in meters. // Maximum Y-coordinate for the radar plot in meters.
export const RADAR_Y_MAX = 60;
export const RADAR_Y_MAX = 80;

113
steps/src/fileParsers.js

@ -1,3 +1,42 @@
/**
* Parses a JSON file stream using Oboe.js to handle very large files.
* @param {string} fileURL - A temporary URL created from the file object.
* @param {function} onProgress - A callback to update the UI on progress.
* @param {function} onComplete - A callback to run when parsing is complete.
* @param {function} onError - A callback to run if an error occurs.
*/
// This function can be deleted if it exists: parseJsonStream
// This function can be deleted if it exists: parseJsonWithOboe
// Add this simplified streaming function
export function parseJsonWithOboe(fileURL, onComplete, onError) {
const vizData = {
radarFrames: [],
tracks: [],
};
oboe(fileURL)
.node("radarFrames[*]", (frame) => {
vizData.radarFrames.push(frame);
return oboe.drop;
})
.node("tracks[*]", (track) => {
vizData.tracks.push(track);
return oboe.drop;
})
.done(() => {
console.log("Oboe.js parsing complete.");
onComplete(vizData);
})
.fail((err) => {
console.error("Oboe.js parsing failed:", err);
onError(
"Error parsing JSON stream. Please check file format and console."
);
});
}
//--------------------CAN-LOG PARSER------------------------// //--------------------CAN-LOG PARSER------------------------//
export function processCanLog(logContent, videoStartDate) { export function processCanLog(logContent, videoStartDate) {
@ -68,72 +107,72 @@ export function processCanLog(logContent, videoStartDate) {
return { data: canData }; return { data: canData };
} }
//--------------------JSON PARSER------------------------//
//--------------------JSON POST-PROCESSOR (ASYNCHRONOUS & SAFE)------------------------//
// Helper function to process large arrays in chunks without blocking
async function processArrayInChunks(array, chunkSize, processingFn) {
for (let i = 0; i < array.length; i += chunkSize) {
const chunk = array.slice(i, i + chunkSize);
processingFn(chunk);
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
export function parseVisualizationJson(
jsonString,
export async function parseVisualizationJson(
vizData,
radarStartTimeMs, radarStartTimeMs,
videoStartDate videoStartDate
) { ) {
try { try {
// Replace Infinity, NaN, and -Infinity with "null" to prevent JSON.parse errors.
const cleanJsonString = jsonString.replace(
/\b(Infinity|NaN|-Infinity)\b/gi,
"null"
);
// Parse the cleaned JSON string into a JavaScript object.
const vizData = JSON.parse(cleanJsonString);
// Validate if the parsed data contains radar frames.
if (!vizData.radarFrames || vizData.radarFrames.length === 0) { if (!vizData.radarFrames || vizData.radarFrames.length === 0) {
return { return {
error: "Error: The JSON file does not contain any radar frames.", error: "Error: The JSON file does not contain any radar frames.",
}; };
} }
// Perform timestamp calculations for each radar frame.
// The `timestampMs` for each frame is calculated relative to the video's start time,
// taking into account the `radarStartTimeMs` (extracted from JSON filename)
// and the `videoStartDate` (extracted from video filename).
// This ensures synchronization between radar data and video.
vizData.radarFrames.forEach((frame) => {
if (videoStartDate && radarStartTimeMs) {
await processArrayInChunks(vizData.radarFrames, 5000, (chunk) => {
chunk.forEach((frame) => {
frame.timestampMs = frame.timestampMs =
radarStartTimeMs + frame.timestamp - videoStartDate.getTime(); radarStartTimeMs + frame.timestamp - videoStartDate.getTime();
}); });
});
}
// Calculate SNR range from the data
let snrValues = [],
totalPoints = 0; // Counter for total points across all frames.
vizData.radarFrames.forEach((frame) => {
let snrValues = [];
let totalPoints = 0;
await processArrayInChunks(vizData.radarFrames, 5000, (chunk) => {
chunk.forEach((frame) => {
if (frame.pointCloud && frame.pointCloud.length > 0) { if (frame.pointCloud && frame.pointCloud.length > 0) {
totalPoints += frame.pointCloud.length; totalPoints += frame.pointCloud.length;
frame.pointCloud.forEach((p) => { frame.pointCloud.forEach((p) => {
// Collect SNR values, ignoring nulls.
if (p.snr !== null) snrValues.push(p.snr); if (p.snr !== null) snrValues.push(p.snr);
}); });
} }
}); });
});
// Warn if no point cloud data was found in the loaded frames.
if (totalPoints === 0) { if (totalPoints === 0) {
console.warn("Warning: Loaded frames contain no point cloud data."); console.warn("Warning: Loaded frames contain no point cloud data.");
} }
// Determine the global minimum and maximum SNR values from the collected data.
// These values are used for scaling the SNR color legend.
// Default to 0 and 1 if no SNR values are found to prevent errors.
const minSnr = snrValues.length > 0 ? Math.min(...snrValues) : 0;
const maxSnr = snrValues.length > 0 ? Math.max(...snrValues) : 1;
// --- FINAL FIX IS HERE ---
// Manually calculate min and max to avoid stack overflow
let minSnr = 0;
let maxSnr = 1;
if (snrValues.length > 0) {
minSnr = snrValues[0];
maxSnr = snrValues[0];
for (let i = 1; i < snrValues.length; i++) {
if (snrValues[i] < minSnr) minSnr = snrValues[i];
if (snrValues[i] > maxSnr) maxSnr = snrValues[i];
}
}
// --- END OF FIX ---
// Return the finished data package
// This object contains the processed visualization data, and the calculated min/max SNR.
return { data: vizData, minSnr: minSnr, maxSnr: maxSnr }; return { data: vizData, minSnr: minSnr, maxSnr: maxSnr };
} catch (error) { } catch (error) {
console.error("JSON Parsing Error:", error);
return {
error:
"Error parsing JSON file. Please check file format. Error: " +
error.message,
};
console.error("JSON Processing Error:", error);
return { error: "Error processing the JSON data. Error: " + error.message };
} }
} }

140
steps/src/main.js

@ -16,16 +16,14 @@
// - main.js: The main application entry point that wires everything // - main.js: The main application entry point that wires everything
// =========================================================================================================== // ===========================================================================================================
// import animation loop from './src/sync.js';
import { animationLoop } from "./sync.js"; import { animationLoop } from "./sync.js";
// import radar sketch from './src/p5/radarSketch.js';
import { radarSketch } from "./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 { 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 {
processCanLog,
parseVisualizationJson,
parseJsonWithOboe,
} from "./fileParsers.js";
import { import {
MAX_TRAJECTORY_LENGTH, MAX_TRAJECTORY_LENGTH,
VIDEO_FPS, VIDEO_FPS,
@ -34,7 +32,6 @@ import {
RADAR_Y_MIN, RADAR_Y_MIN,
RADAR_Y_MAX, RADAR_Y_MAX,
} from "./constants.js"; } from "./constants.js";
// import utils and helpers from './src/utils.js';
import { import {
findRadarFrameIndexForTime, findRadarFrameIndexForTime,
findLastCanIndexBefore, findLastCanIndexBefore,
@ -42,11 +39,8 @@ import {
parseTimestamp, parseTimestamp,
throttle, throttle,
} from "./utils.js"; } from "./utils.js";
// import state machine from './src/state.js';
import { appState } from "./state.js"; import { appState } from "./state.js";
// import DOM elements and UI updaters from './src/dom.js';
import { import {
//---DOM Elements---//
canvasContainer, canvasContainer,
canvasPlaceholder, canvasPlaceholder,
videoPlayer, videoPlayer,
@ -86,17 +80,13 @@ import {
speedGraphContainer, speedGraphContainer,
speedGraphPlaceholder, speedGraphPlaceholder,
toggleCloseUp, toggleCloseUp,
//---UI Updaters---//
updateFrame, updateFrame,
resetVisualization, resetVisualization,
updateCanDisplay, updateCanDisplay,
updateDebugOverlay, updateDebugOverlay,
} from "./dom.js"; } from "./dom.js";
// Import modal dialog logic from './src/modal.js'.
import { showModal } from "./modal.js"; import { showModal } from "./modal.js";
// Import theme initialization from './src/theme.js'.
import { initializeTheme } from "./theme.js"; import { initializeTheme } from "./theme.js";
// Import caching logic from './src/db.js'.
import { initDB, saveFileToDB, loadFileFromDB } from "./db.js"; import { initDB, saveFileToDB, loadFileFromDB } from "./db.js";
// Sets up the video player with the given file URL. // Sets up the video player with the given file URL.
@ -106,10 +96,12 @@ function setupVideoPlayer(fileURL) {
videoPlaceholder.classList.add("hidden"); videoPlaceholder.classList.add("hidden");
videoPlayer.playbackRate = parseFloat(speedSlider.value); videoPlayer.playbackRate = parseFloat(speedSlider.value);
} }
// Event listener for loading JSON file. // Event listener for loading JSON file.
loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); loadJsonBtn.addEventListener("click", () => jsonFileInput.click());
loadVideoBtn.addEventListener("click", () => videoFileInput.click()); loadVideoBtn.addEventListener("click", () => videoFileInput.click());
loadCanBtn.addEventListener("click", () => canFileInput.click()); loadCanBtn.addEventListener("click", () => canFileInput.click());
clearCacheBtn.addEventListener("click", async () => { clearCacheBtn.addEventListener("click", async () => {
const confirmed = await showModal("Clear all cached data and reload?", true); const confirmed = await showModal("Clear all cached data and reload?", true);
if (confirmed) { if (confirmed) {
@ -118,41 +110,48 @@ clearCacheBtn.addEventListener("click", async () => {
window.location.reload(); window.location.reload();
} }
}); });
// Event listener for JSON file input change.
jsonFileInput.addEventListener("change", (event) => { jsonFileInput.addEventListener("change", (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
appState.jsonFilename = file.name; appState.jsonFilename = file.name;
localStorage.setItem("jsonFilename", appState.jsonFilename); localStorage.setItem("jsonFilename", appState.jsonFilename);
calculateAndSetOffset(); // This function now correctly sets appState variables
calculateAndSetOffset();
const reader = new FileReader();
reader.onload = (e) => {
const jsonString = e.target.result;
saveFileToDB("json", jsonString);
const fileURL = URL.createObjectURL(file);
// 1. Give the raw ingredients to our new JSON "chef"
const result = parseVisualizationJson(
jsonString,
// Show a simple "parsing" message
showModal("Parsing large JSON file, please wait...");
const onError = (errorMessage) => {
URL.revokeObjectURL(fileURL);
showModal(errorMessage);
};
const onComplete = async (parsedData) => {
URL.revokeObjectURL(fileURL);
showModal("Processing data...");
const result = await parseVisualizationJson(
parsedData,
appState.radarStartTimeMs, appState.radarStartTimeMs,
appState.videoStartDate appState.videoStartDate
); );
// 2. Check the result
if (result.error) { if (result.error) {
showModal(result.error); showModal(result.error);
return; return;
} }
// 3. Update the application's central state with the prepared data
appState.vizData = result.data; appState.vizData = result.data;
appState.globalMinSnr = result.minSnr; appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr; appState.globalMaxSnr = result.maxSnr;
// 4. Now, the "waiter" updates the UI
// Update UI
snrMinInput.value = appState.globalMinSnr.toFixed(1); snrMinInput.value = appState.globalMinSnr.toFixed(1);
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); snrMaxInput.value = appState.globalMaxSnr.toFixed(1);
resetVisualization(); // This UI function is in dom.js
resetVisualization();
canvasPlaceholder.style.display = "none"; canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden"); featureToggles.classList.remove("hidden");
@ -160,23 +159,13 @@ jsonFileInput.addEventListener("change", (event) => {
appState.p5_instance = new p5(radarSketch); 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();
}
document.getElementById("modal-ok-btn").click();
}; };
reader.readAsText(file);
// Start the simple parsing process
parseJsonWithOboe(fileURL, onComplete, onError);
}); });
// Event listener for video file input change. // Event listener for video file input change.
videoFileInput.addEventListener("change", (event) => { videoFileInput.addEventListener("change", (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
@ -185,10 +174,8 @@ videoFileInput.addEventListener("change", (event) => {
localStorage.setItem("videoFilename", appState.videoFilename); localStorage.setItem("videoFilename", appState.videoFilename);
saveFileToDB("video", file); saveFileToDB("video", file);
// This is the key moment: we now have a video start date.
calculateAndSetOffset(); calculateAndSetOffset();
// Now, check if we have pending data that needs this date.
if (appState.rawCanLogText) { if (appState.rawCanLogText) {
const result = processCanLog( const result = processCanLog(
appState.rawCanLogText, appState.rawCanLogText,
@ -200,7 +187,6 @@ videoFileInput.addEventListener("change", (event) => {
} }
} }
// NEW: Re-process vizData if it was loaded before the video.
if (appState.vizData) { if (appState.vizData) {
console.log("DEBUG: Video loaded after JSON. Re-calculating timestamps."); console.log("DEBUG: Video loaded after JSON. Re-calculating timestamps.");
appState.vizData.radarFrames.forEach((frame) => { appState.vizData.radarFrames.forEach((frame) => {
@ -209,13 +195,12 @@ videoFileInput.addEventListener("change", (event) => {
frame.timestamp - frame.timestamp -
appState.videoStartDate.getTime(); appState.videoStartDate.getTime();
}); });
resetVisualization(); // Reset UI to reflect new timestamps
resetVisualization();
} }
const fileURL = URL.createObjectURL(file); const fileURL = URL.createObjectURL(file);
setupVideoPlayer(fileURL); setupVideoPlayer(fileURL);
// When the video is ready, update the speed graph
videoPlayer.onloadedmetadata = () => { videoPlayer.onloadedmetadata = () => {
if (appState.speedGraphInstance) { if (appState.speedGraphInstance) {
appState.speedGraphInstance.setData( appState.speedGraphInstance.setData(
@ -226,6 +211,7 @@ videoFileInput.addEventListener("change", (event) => {
} }
}; };
}); });
// Event listener for CAN file input change. // Event listener for CAN file input change.
canFileInput.addEventListener("change", (event) => { canFileInput.addEventListener("change", (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
@ -238,26 +224,20 @@ canFileInput.addEventListener("change", (event) => {
const logContent = e.target.result; const logContent = e.target.result;
saveFileToDB("canLogText", logContent); saveFileToDB("canLogText", logContent);
// 1. Give the raw ingredients to the chef (our parser)
const result = processCanLog(logContent, appState.videoStartDate); const result = processCanLog(logContent, appState.videoStartDate);
// 2. Check what the chef gave back
if (result.error) { if (result.error) {
// If there was an error, show it and save the raw text for later.
showModal(result.error); showModal(result.error);
appState.rawCanLogText = result.rawCanLogText; appState.rawCanLogText = result.rawCanLogText;
return; return;
} }
// 3. If successful, update the application's central state
appState.canData = result.data; appState.canData = result.data;
appState.rawCanLogText = null; appState.rawCanLogText = null;
// 4. Now, the waiter updates the UI based on the new state
if (appState.canData.length > 0 || appState.vizData) { if (appState.canData.length > 0 || appState.vizData) {
speedGraphPlaceholder.classList.add("hidden"); speedGraphPlaceholder.classList.add("hidden");
if (!appState.speedGraphInstance) { if (!appState.speedGraphInstance) {
// We need to pass the speedGraphSketch function definition here
appState.speedGraphInstance = new p5(speedGraphSketch); appState.speedGraphInstance = new p5(speedGraphSketch);
} }
if (videoPlayer.duration) { if (videoPlayer.duration) {
@ -273,11 +253,13 @@ canFileInput.addEventListener("change", (event) => {
}; };
reader.readAsText(file); reader.readAsText(file);
}); });
// Event listener for offset input change. // Event listener for offset input change.
offsetInput.addEventListener("input", () => { offsetInput.addEventListener("input", () => {
autoOffsetIndicator.classList.add("hidden"); autoOffsetIndicator.classList.add("hidden");
localStorage.setItem("visualizerOffset", offsetInput.value); localStorage.setItem("visualizerOffset", offsetInput.value);
}); });
// Event listener for apply SNR button click. // Event listener for apply SNR button click.
applySnrBtn.addEventListener("click", () => { applySnrBtn.addEventListener("click", () => {
const newMin = parseFloat(snrMinInput.value), const newMin = parseFloat(snrMinInput.value),
@ -297,6 +279,7 @@ applySnrBtn.addEventListener("click", () => {
appState.p5_instance.redraw(); appState.p5_instance.redraw();
} }
}); });
// Event listener for play/pause button click. // Event listener for play/pause button click.
playPauseBtn.addEventListener("click", () => { playPauseBtn.addEventListener("click", () => {
if (!appState.vizData && !videoPlayer.src) return; if (!appState.vizData && !videoPlayer.src) return;
@ -314,6 +297,7 @@ playPauseBtn.addEventListener("click", () => {
if (videoPlayer.src) videoPlayer.pause(); if (videoPlayer.src) videoPlayer.pause();
} }
}); });
// Event listener for stop button click. // Event listener for stop button click.
stopBtn.addEventListener("click", () => { stopBtn.addEventListener("click", () => {
videoPlayer.pause(); videoPlayer.pause();
@ -326,6 +310,7 @@ stopBtn.addEventListener("click", () => {
} }
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
}); });
// Event listener for timeline slider input. // Event listener for timeline slider input.
timelineSlider.addEventListener( timelineSlider.addEventListener(
"input", "input",
@ -341,8 +326,8 @@ timelineSlider.addEventListener(
appState.mediaTimeStart = videoPlayer.currentTime; appState.mediaTimeStart = videoPlayer.currentTime;
appState.masterClockStart = performance.now(); appState.masterClockStart = performance.now();
}, 16) }, 16)
); // Throttle delay for smoother updates.
// Currently set at 16 ms to achieve smooth 60fps.
);
// Event listener for speed slider input. // Event listener for speed slider input.
speedSlider.addEventListener("input", (event) => { speedSlider.addEventListener("input", (event) => {
const speed = parseFloat(event.target.value); const speed = parseFloat(event.target.value);
@ -350,8 +335,6 @@ speedSlider.addEventListener("input", (event) => {
speedDisplay.textContent = `${speed.toFixed(1)}x`; speedDisplay.textContent = `${speed.toFixed(1)}x`;
}); });
// ADD THE NEW TOGGLE TO THE ARRAY
// Array of color toggles.
const colorToggles = [ const colorToggles = [
toggleSnrColor, toggleSnrColor,
toggleClusterColor, toggleClusterColor,
@ -368,7 +351,7 @@ colorToggles.forEach((t) => {
if (appState.p5_instance) appState.p5_instance.redraw(); if (appState.p5_instance) appState.p5_instance.redraw();
}); });
}); });
// Event listeners for various feature toggles.
[ [
toggleVelocity, toggleVelocity,
toggleEgoSpeed, toggleEgoSpeed,
@ -391,7 +374,7 @@ colorToggles.forEach((t) => {
} }
}); });
}); });
// Event listener for close-up toggle.
toggleCloseUp.addEventListener("change", () => { toggleCloseUp.addEventListener("change", () => {
appState.isCloseUpMode = toggleCloseUp.checked; appState.isCloseUpMode = toggleCloseUp.checked;
if (appState.p5_instance) { if (appState.p5_instance) {
@ -406,12 +389,12 @@ toggleCloseUp.addEventListener("change", () => {
} }
} }
}); });
// Event listener for video ended event.
videoPlayer.addEventListener("ended", () => { videoPlayer.addEventListener("ended", () => {
appState.isPlaying = false; appState.isPlaying = false;
playPauseBtn.textContent = "Play"; playPauseBtn.textContent = "Play";
}); });
// Event listener for keyboard arrow key presses to navigate frames.
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if ( if (
!appState.vizData || !appState.vizData ||
@ -438,7 +421,7 @@ document.addEventListener("keydown", (event) => {
appState.masterClockStart = performance.now(); appState.masterClockStart = performance.now();
} }
}); });
// Calculates and sets the time offset between JSON and video timestamps.
function calculateAndSetOffset() { function calculateAndSetOffset() {
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
@ -472,10 +455,10 @@ function calculateAndSetOffset() {
} }
} }
// Application Initialization: Event listener for DOMContentLoaded.
// Application Initialization
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initializeTheme(); initializeTheme();
console.log("DEBUG: DOMContentLoaded fired. Starting session load."); // Log for debugging.
console.log("DEBUG: DOMContentLoaded fired. Starting session load.");
initDB(() => { initDB(() => {
console.log("DEBUG: Database initialized."); console.log("DEBUG: Database initialized.");
@ -487,10 +470,8 @@ document.addEventListener("DOMContentLoaded", () => {
appState.jsonFilename = localStorage.getItem("jsonFilename"); appState.jsonFilename = localStorage.getItem("jsonFilename");
appState.canLogFilename = localStorage.getItem("canLogFilename"); appState.canLogFilename = localStorage.getItem("canLogFilename");
// This is important: it sets videoStartDate if a video filename is cached
calculateAndSetOffset(); // Calculate offset based on cached filenames.
calculateAndSetOffset();
// Promises to load files from IndexedDB.
const videoPromise = new Promise((resolve) => const videoPromise = new Promise((resolve) =>
loadFileFromDB("video", resolve) loadFileFromDB("video", resolve)
); );
@ -500,18 +481,22 @@ document.addEventListener("DOMContentLoaded", () => {
const canLogPromise = new Promise((resolve) => const canLogPromise = new Promise((resolve) =>
loadFileFromDB("canLogText", resolve) loadFileFromDB("canLogText", resolve)
); );
// Once all files are loaded from DB, process them.
Promise.all([videoPromise, jsonPromise, canLogPromise]) Promise.all([videoPromise, jsonPromise, canLogPromise])
.then(([videoBlob, jsonString, canLogText]) => { .then(([videoBlob, jsonString, canLogText]) => {
console.log("DEBUG: All data fetched from IndexedDB."); console.log("DEBUG: All data fetched from IndexedDB.");
const processAllData = () => {
const processAllData = async () => {
console.log("DEBUG: Processing all loaded data."); console.log("DEBUG: Processing all loaded data.");
// 1. Process JSON (only if video start date is available).
if (jsonString && appState.videoStartDate) { if (jsonString && appState.videoStartDate) {
try {
// 1. First, parse the string from the cache into an object.
const cachedData = JSON.parse(jsonString);
// 2. Now, pass the OBJECT to our updated function.
const result = parseVisualizationJson( const result = parseVisualizationJson(
jsonString,
cachedData, // Pass the object directly
appState.radarStartTimeMs, appState.radarStartTimeMs,
appState.videoStartDate appState.videoStartDate
); );
@ -524,9 +509,14 @@ document.addEventListener("DOMContentLoaded", () => {
} else { } else {
showModal(result.error); showModal(result.error);
} }
} catch (e) {
showModal(
"Error parsing cached JSON data. Please clear cache and reload."
);
console.error("DEBUG: Error parsing cached JSON data:", e);
}
} }
// 2. Process CAN log (only if video start date is available).
if (canLogText && appState.videoStartDate) { if (canLogText && appState.videoStartDate) {
const result = processCanLog(canLogText, appState.videoStartDate); const result = processCanLog(canLogText, appState.videoStartDate);
if (!result.error) { if (!result.error) {
@ -534,7 +524,6 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// 3. Update all UI elements now that data is processed.
if (appState.vizData) { if (appState.vizData) {
resetVisualization(); resetVisualization();
canvasPlaceholder.style.display = "none"; canvasPlaceholder.style.display = "none";
@ -556,14 +545,11 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}; };
// Main controller for processing data based on video availability.
if (videoBlob) { if (videoBlob) {
const fileURL = URL.createObjectURL(videoBlob); const fileURL = URL.createObjectURL(videoBlob);
setupVideoPlayer(fileURL); setupVideoPlayer(fileURL);
// This ensures we ONLY process data once the video's duration is known.
videoPlayer.onloadedmetadata = processAllData; videoPlayer.onloadedmetadata = processAllData;
} else { } else {
// If there's no video, process other data immediately.
processAllData(); processAllData();
} }
}) })

617
steps/src/main_old.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);
});
});
});

106
steps/src/parser.worker.js

@ -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.' });
}
};
Loading…
Cancel
Save