Browse Source

Acieved progress bar to Large JSON loading and caching.

refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
1a1084376f
  1. 7
      steps/index.html
  2. 36
      steps/src/dom.js
  3. 10
      steps/src/fileParsers.js
  4. 169
      steps/src/main.js
  5. 30
      steps/src/modal.js
  6. 85
      steps/src/parser.worker.js

7
steps/index.html

@ -269,6 +269,13 @@
<div id="modal-content"
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md z-10 transform scale-95">
<p id="modal-text" class="text-gray-700 dark:text-gray-300 mb-4"></p>
<div id="modal-progress-container" class="hidden w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-600 my-4">
<div id="modal-progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style="width: 0%"></div>
</div>
<span id="modal-progress-text" class="text-sm text-gray-500 dark:text-gray-400 mt-1 block text-center"></span>
<div id="modal-buttons" class="flex justify-end gap-4">
</div>
<div id="modal-buttons" class="flex justify-end gap-4">
<button id="modal-cancel-btn"
class="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold">

36
steps/src/dom.js

@ -22,39 +22,25 @@ export const speedSlider = document.getElementById("speed-slider");
export const speedDisplay = document.getElementById("speed-display");
export const featureToggles = document.getElementById("feature-toggles");
export const toggleSnrColor = document.getElementById("toggle-snr-color");
export const toggleClusterColor = document.getElementById(
"toggle-cluster-color"
);
export const toggleClusterColor = document.getElementById("toggle-cluster-color");
export const toggleInlierColor = document.getElementById("toggle-inlier-color");
export const toggleStationaryColor = document.getElementById(
"toggle-stationary-color"
);
export const toggleStationaryColor = document.getElementById("toggle-stationary-color");
export const toggleVelocity = document.getElementById("toggle-velocity");
export const toggleTracks = document.getElementById("toggle-tracks");
export const toggleEgoSpeed = document.getElementById("toggle-ego-speed");
export const toggleFrameNorm = document.getElementById("toggle-frame-norm");
export const toggleDebugOverlay = document.getElementById(
"toggle-debug-overlay"
);
export const toggleDebugOverlay = document.getElementById("toggle-debug-overlay");
export const egoSpeedDisplay = document.getElementById("ego-speed-display");
export const canSpeedDisplay = document.getElementById("can-speed-display");
export const debugOverlay = document.getElementById("debug-overlay");
export const toggleDebug2Overlay = document.getElementById(
"toggle-debug2-overlay"
);
export const toggleDebug2Overlay = document.getElementById("toggle-debug2-overlay");
export const snrMinInput = document.getElementById("snr-min-input");
export const snrMaxInput = document.getElementById("snr-max-input");
export const applySnrBtn = document.getElementById("apply-snr-btn");
export const autoOffsetIndicator = document.getElementById(
"auto-offset-indicator"
);
export const autoOffsetIndicator = document.getElementById("auto-offset-indicator");
export const clearCacheBtn = document.getElementById("clear-cache-btn");
export const speedGraphContainer = document.getElementById(
"speed-graph-container"
);
export const speedGraphPlaceholder = document.getElementById(
"speed-graph-placeholder"
);
export const speedGraphContainer = document.getElementById("speed-graph-container");
export const speedGraphPlaceholder = document.getElementById("speed-graph-placeholder");
export const modalContainer = document.getElementById("modal-container");
export const modalOverlay = document.getElementById("modal-overlay");
export const modalContent = document.getElementById("modal-content");
@ -62,10 +48,12 @@ export const modalText = document.getElementById("modal-text");
export const modalOkBtn = document.getElementById("modal-ok-btn");
export const modalCancelBtn = document.getElementById("modal-cancel-btn");
export const toggleCloseUp = document.getElementById("toggle-close-up");
export const togglePredictedPos = document.getElementById(
"toggle-predicted-pos"
);
export const togglePredictedPos = document.getElementById("toggle-predicted-pos");
export const toggleCovariance = document.getElementById("toggle-covariance");
// In src/dom.js, add these exports
export const modalProgressContainer = document.getElementById("modal-progress-container");
export const modalProgressBar = document.getElementById("modal-progress-bar");
export const modalProgressText = document.getElementById("modal-progress-text");
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.

10
steps/src/fileParsers.js

@ -9,7 +9,7 @@
// This function can be deleted if it exists: parseJsonWithOboe
// Add this simplified streaming function
export function parseJsonWithOboe(fileURL, onComplete, onError) {
export function parseJsonWithOboe(fileURL, onComplete, onError, onProgress) {
const vizData = {
radarFrames: [],
tracks: [],
@ -24,6 +24,13 @@ export function parseJsonWithOboe(fileURL, onComplete, onError) {
vizData.tracks.push(track);
return oboe.drop;
})
// Add the progress listener
.on("progress", (progress) => {
// Oboe.js provides a progress object with a 'percent' property
if (onProgress) {
onProgress(progress.percent);
}
})
.done(() => {
console.log("Oboe.js parsing complete.");
onComplete(vizData);
@ -36,7 +43,6 @@ export function parseJsonWithOboe(fileURL, onComplete, onError) {
});
}
//--------------------JSON POST-PROCESSOR (ASYNCHRONOUS & SAFE)------------------------//
// Helper function to process large arrays in chunks without blocking

169
steps/src/main.js

@ -16,6 +16,7 @@
// - main.js: The main application entry point that wires everything
// ===========================================================================================================
import { showModal, updateModalProgress } from "./modal.js"; // Modify this import
import { animationLoop } from "./sync.js";
import { radarSketch } from "./p5/radarSketch.js";
import { speedGraphSketch } from "./p5/speedGraphSketch.js";
@ -76,7 +77,6 @@ import {
resetVisualization,
updateDebugOverlay,
} from "./dom.js";
import { showModal } from "./modal.js";
import { initializeTheme } from "./theme.js";
import { initDB, saveFileToDB, loadFileFromDB } from "./db.js";
@ -101,103 +101,78 @@ clearCacheBtn.addEventListener("click", async () => {
}
});
// In src/main.js, REPLACE the jsonFileInput event listener with this:
jsonFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
appState.jsonFilename = file.name;
localStorage.setItem("jsonFilename", appState.jsonFilename);
calculateAndSetOffset();
saveFileToDB("json", file); // We still cache the raw file
saveFileToDB("json", file); // Save the file object for the next session
// 1. Show a loading modal immediately.
showModal("Parsing large JSON file, please wait...");
// 2. Create a temporary URL for the streaming parser.
const fileURL = URL.createObjectURL(file);
// 3. Use the robust streaming parser.
// 1. Show the modal with the progress bar
showModal("Parsing large JSON file...", false, true);
updateModalProgress(0);
parseJsonWithOboe(
fileURL,
// 2. Create a new Worker from our script
const worker = new Worker('./src/parser.worker.js');
async (parsedData) => {
// This is the success callback, running after the file is parsed.
// 3. Set up listeners for messages FROM the worker
worker.onmessage = async (e) => {
const { type, data, message, percent } = e.data;
// We make it async so we can `await` the next step.
if (type === 'progress') {
// Update the progress bar whenever the worker reports progress
updateModalProgress(percent);
} else if (type === 'complete') {
// Worker is done! Process the data it sent back.
updateModalProgress(100);
const result = await parseVisualizationJson(
parsedData,
data, // Use the data object directly from the worker
appState.radarStartTimeMs,
appState.videoStartDate
);
// Revoke the temporary URL to free up memory.
URL.revokeObjectURL(fileURL);
if (result.error) {
showModal(result.error);
return;
}
appState.vizData = result.data;
appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr;
// Update UI with the correct, awaited data.
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);
}
if (appState.vizData) {
speedGraphPlaceholder.classList.add("hidden");
if (appState.vizData && videoPlayer.duration) {
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
if (videoPlayer.duration) {
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
}
appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration);
}
// Close the loading modal.
// Close the modal and terminate the worker
document.getElementById("modal-ok-btn").click();
},
worker.terminate();
(error) => {
// This is the error callback for the streaming parser.
showModal(error);
URL.revokeObjectURL(fileURL);
} else if (type === 'error') {
// The worker ran into an error
showModal(message);
worker.terminate();
}
);
};
// 4. Send the file TO the worker to start the job
worker.postMessage({ file: file });
});
// Event listener for video file input change.
@ -452,6 +427,8 @@ function calculateAndSetOffset() {
}
// Application Initialization
// In src/main.js, REPLACE the entire 'DOMContentLoaded' listener with this:
document.addEventListener("DOMContentLoaded", () => {
initializeTheme();
console.log("DEBUG: DOMContentLoaded fired. Starting session load.");
@ -467,22 +444,15 @@ document.addEventListener("DOMContentLoaded", () => {
calculateAndSetOffset();
const videoPromise = new Promise((resolve) =>
loadFileFromDB("video", resolve)
);
const jsonPromise = new Promise((resolve) =>
loadFileFromDB("json", resolve)
);
// At the end of main.js, inside the DOMContentLoaded listener
const videoPromise = new Promise((resolve) => loadFileFromDB("video", resolve));
const jsonPromise = new Promise((resolve) => loadFileFromDB("json", resolve));
Promise.all([videoPromise, jsonPromise])
.then(([videoBlob, jsonBlob]) => {
// Renamed jsonString to jsonBlob
console.log("DEBUG: All data fetched from IndexedDB.");
const processRestOfData = async (parsedJson) => {
// This is our main processing logic
// This function will be called with the fully parsed JSON data when ready.
const finalizeSetup = async (parsedJson) => {
if (parsedJson) {
const result = await parseVisualizationJson(
parsedJson,
@ -492,7 +462,8 @@ document.addEventListener("DOMContentLoaded", () => {
if (!result.error) {
appState.vizData = result.data;
// ... (update UI elements)
appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr;
snrMinInput.value = result.minSnr.toFixed(1);
snrMaxInput.value = result.maxSnr.toFixed(1);
} else {
@ -500,6 +471,12 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
// Setup video player
if (videoBlob) {
const fileURL = URL.createObjectURL(videoBlob);
setupVideoPlayer(fileURL);
}
// Final UI updates
if (appState.vizData) {
resetVisualization();
@ -509,47 +486,35 @@ document.addEventListener("DOMContentLoaded", () => {
appState.p5_instance = new p5(radarSketch);
}
}
if (appState.vizData) {
speedGraphPlaceholder.classList.add("hidden");
if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch);
}
if (videoPlayer.duration) {
appState.speedGraphInstance.setData(
appState.vizData,
videoPlayer.duration
);
}
}
};
// Main controller for loading cached data
if (videoBlob) {
const fileURL = URL.createObjectURL(videoBlob);
setupVideoPlayer(fileURL);
videoPlayer.onloadedmetadata = () => {
if (jsonBlob) {
// If a JSON blob exists, parse it first
const jsonUrl = URL.createObjectURL(jsonBlob);
parseJsonWithOboe(
jsonUrl,
(data) => processRestOfData(data),
(err) => showModal(err)
);
} else {
processRestOfData(null);
// --- CACHED JSON FOUND: USE WORKER ---
showModal("Loading data from cache...", false, true);
updateModalProgress(0);
const worker = new Worker('./src/parser.worker.js');
worker.onmessage = async (e) => {
const { type, data, message, percent } = e.data;
if (type === 'progress') {
updateModalProgress(percent);
} else if (type === 'complete') {
updateModalProgress(100);
await finalizeSetup(data);
document.getElementById("modal-ok-btn").click();
worker.terminate();
} else if (type === 'error') {
showModal(message);
worker.terminate();
}
};
} else if (jsonBlob) {
// If there's no video but there is a JSON blob
const jsonUrl = URL.createObjectURL(jsonBlob);
parseJsonWithOboe(
jsonUrl,
(data) => processRestOfData(data),
(err) => showModal(err)
);
worker.postMessage({ file: jsonBlob });
} else {
processRestOfData(null); // No cached data to process
// --- NO CACHED JSON ---
finalizeSetup(null);
}
})
.catch((error) => {

30
steps/src/modal.js

@ -1,21 +1,31 @@
import {
modalText,
modalCancelBtn,
modalContainer,
modalOverlay,
modalContent,
} from "./dom.js";
// First, import the new DOM elements at the top
import {
modalText,
//...
modalOkBtn,
modalProgressContainer, // Add this
modalProgressBar, // Add this
modalProgressText, // Add this
} from "./dom.js";
// --- Custom Modal Logic --- //
// Variable to store the resolve function of the Promise, allowing the modal to return a value.
let modalResolve = null;
export function showModal(message, isConfirm = false) {
export function showModal(message, isConfirm = false, showProgress = false) {
return new Promise((resolve) => {
// Set the message text for the modal.
modalText.textContent = message;
// Show/hide the cancel button based on whether it's a confirmation modal.
modalCancelBtn.classList.toggle("hidden", !isConfirm);
modalProgressContainer.classList.toggle("hidden", !showProgress);
// Make the modal container visible.
modalContainer.classList.remove("hidden");
// Add a slight delay for CSS transitions to take effect, making the modal appear smoothly.
@ -27,12 +37,28 @@ export function showModal(message, isConfirm = false) {
modalResolve = resolve;
});
}
// Add this new exported function to update the progress bar
export function updateModalProgress(percent) {
if (modalProgressBar && modalProgressText) {
const p = Math.round(percent);
modalProgressBar.style.width = `${p}%`;
modalProgressText.textContent =
p < 100 ? `Parsing... ${p}%` : "Finalizing...";
}
}
// Hides the modal and resolves the Promise with the given value.
function hideModal(value) {
modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95");
setTimeout(() => {
modalContainer.classList.add("hidden");
// Reset progress bar for the next time
if (modalProgressContainer && modalProgressBar && modalProgressText) {
modalProgressContainer.classList.add("hidden");
modalProgressBar.style.width = "0%";
modalProgressText.textContent = "";
}
if (modalResolve) modalResolve(value);
}, 200);
}

85
steps/src/parser.worker.js

@ -1,3 +1,5 @@
// In src/parser.worker.js
// Import the lightweight and worker-safe Clarinet library
importScripts('https://cdn.jsdelivr.net/npm/clarinet@0.12.5/clarinet.min.js');
@ -9,69 +11,62 @@ self.onmessage = async function(event) {
}
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;
// --- START: New Robust Parsing Logic ---
// This logic correctly builds a complete object tree from the stream.
let result; // This will hold the final, fully parsed object.
const stack = []; // A stack to keep track of current object/array.
let key = null; // The current object key.
const getParent = () => stack.length > 0 ? stack[stack.length - 1] : null;
const assign = (value) => {
const parent = getParent();
if (parent) {
if (Array.isArray(parent)) {
parent.push(value);
} else {
parent[key] = value;
}
} else {
result = value;
}
};
parser.onopenobject = () => {
// We only care about objects inside our target arrays
if (state.inRadarFrames || state.inTracks) {
state.currentObject = {};
}
parser.onopenobject = (k) => {
key = k;
const newObject = {};
assign(newObject);
stack.push(newObject);
};
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.onkey = (k) => {
key = k;
};
parser.onclosearray = () => {
// When we finish an array, update our state
if (state.inRadarFrames) state.inRadarFrames = false;
if (state.inTracks) state.inTracks = false;
parser.onopenarray = () => {
const newArray = [];
assign(newArray);
stack.push(newArray);
};
parser.onvalue = (value) => {
if (state.currentObject && state.currentKey) {
state.currentObject[state.currentKey] = value;
}
assign(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 ---
parser.oncloseobject = () => stack.pop();
parser.onclosearray = () => stack.pop();
// --- END: New Robust Parsing Logic ---
parser.onend = () => {
self.postMessage({ type: 'progress', percent: 100 });
self.postMessage({ type: 'complete', data: vizData });
// Send the fully constructed 'result' object back to the main thread.
self.postMessage({ type: 'complete', data: result });
};
parser.onerror = (err) => {
@ -79,7 +74,7 @@ self.onmessage = async function(event) {
self.postMessage({ type: 'error', message: 'Failed to parse JSON structure.' });
};
// --- Stream Reading Logic (remains the same) ---
// --- Stream Reading Logic (this part remains the same) ---
const stream = file.stream();
const reader = stream.getReader();
const decoder = new TextDecoder();

Loading…
Cancel
Save