From f927351c2c7dfcf1fd52a7385456980fb84bb09d Mon Sep 17 00:00:00 2001 From: rakadu1 Date: Fri, 22 Aug 2025 15:48:07 +0530 Subject: [PATCH] fix(refactor): Resolve critical loading, theme, and resize bugs This commit resolves three persistent bugs that were identified during the refactoring of the file parsers and UI modules. 1. Fixed Cache Loading Race Condition Problem: On page load, the application would try to process cached JSON data immediately. If the cached video file hadn't finished loading its metadata yet, the videoStartDate would be null, causing a getTime of null error in the JSON parser and preventing the visualization from loading. Solution: The application's startup logic in DOMContentLoaded has been rewritten. A new processAllData function now contains all the data parsing logic. This function is now called exclusively as a callback to the video player's onloadedmetadata event. This guarantees that all data processing (for JSON and CAN logs) only happens after the video is ready and a valid videoStartDate is available. 2. Fixed Speed Graph Theme Not Updating Problem: The speed graph's background, grid, and data lines are drawn to a static buffer for performance. When the theme was changed, the setTheme function would call .redraw() on the sketch, but this would only re-display the old buffer with the old theme's colors. The buffer itself was not being regenerated. Solution: The drawStaticGraphToBuffer function within the speedGraphSketch was made public by attaching it to the p5 instance (p.drawStaticGraphToBuffer). The setTheme function in src/theme.js was updated to explicitly call this new public function. It now forces the speed graph to generate a new static buffer with the correct theme colors before redrawing. Imported the videoPlayer element into theme.js to prevent a ReferenceError when checking its properties. 3. Fixed p5.js Canvas Resizing Error Problem: When the browser window was resized, both p5 sketches would throw a TypeError because the code was attempting to call .resize() on the static graphics buffers (staticBackgroundBuffer and staticBuffer), which is not a valid function. Solution: The windowResized functions in both sketches were corrected. Instead of trying to resize the existing buffer, the code now creates a brand new, correctly-sized buffer using p.createGraphics() and immediately redraws the static content (axes, gridlines, etc.) onto it. --- steps/index.html | 359 ++++++++++++++++++++++++++++----------- steps/src/fileParsers.js | 88 ++++++++++ steps/src/theme.js | 16 +- 3 files changed, 361 insertions(+), 102 deletions(-) create mode 100644 steps/src/fileParsers.js diff --git a/steps/index.html b/steps/index.html index f9c3597..bc4d324 100644 --- a/steps/index.html +++ b/steps/index.html @@ -263,6 +263,15 @@ // =========================================================================================================== + + + // import JSON parser, can log procesor from './src/fileParsers.js'; + + import { + processCanLog, parseVisualizationJson + } from './src/fileParsers.js'; + + // import constants from './constants.js'; import { MAX_TRAJECTORY_LENGTH, @@ -315,12 +324,12 @@ } from './src/theme.js'; // import caching logic from './src/db.js'; - import { + import { initDB, saveFileToDB, loadFileFromDB } from './src/db.js'; // import file parsers from './src/fileParsers.js';' - + // --- p5.js Sketch Definitions --- let sketch = function (p) { @@ -647,34 +656,23 @@ } p.drawSnrLegendToBuffer = function (minV, maxV) { const b = snrLegendBuffer; b.clear(); b.push(); const lx = 10, ly = 20, lw = 15, lh = 400; for (let i = 0; i < lh; i++) { const amt = b.map(i, 0, lh, 1, 0); let c; if (amt < 0.25) c = b.lerpColor(snrColors.c1, snrColors.c2, amt / 0.25); else if (amt < 0.5) c = b.lerpColor(snrColors.c2, snrColors.c3, (amt - 0.25) / 0.25); else if (amt < 0.75) c = b.lerpColor(snrColors.c3, snrColors.c4, (amt - 0.5) / 0.25); else c = b.lerpColor(snrColors.c4, snrColors.c5, (amt - 0.75) / 0.25); b.stroke(c); b.line(lx, ly + i, lx + lw, ly + i); } b.fill(0); b.noStroke(); b.textSize(10); b.textAlign(b.LEFT, b.CENTER); b.text(maxV.toFixed(1), lx + lw + 5, ly); b.text(minV.toFixed(1), lx + lw + 5, ly + lh); b.text("SNR", lx, ly - 10); b.pop(); }; - p.windowResized = function () { p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight); staticBackgroundBuffer.resize(p.width, p.height); calculatePlotScales(); drawStaticRegionsToBuffer(); if (appState.vizData) p.redraw(); }; + p.windowResized = function () { + p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight); + // Instead of resizing the buffer, we re-create it + staticBackgroundBuffer = p.createGraphics(p.width, p.height); + // And we must re-draw the static content to the new buffer + calculatePlotScales(); + drawStaticRegionsToBuffer(); + if (appState.vizData) p.redraw(); + }; }; let speedGraphSketch = function (p) { let staticBuffer, minSpeed, maxSpeed, videoDuration; const pad = { top: 20, right: 130, bottom: 30, left: 50 }; - p.setup = function () { let canvas = p.createCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight); canvas.parent('speed-graph-container'); staticBuffer = p.createGraphics(p.width, p.height); p.noLoop(); }; - p.setData = function (canSpeedData, radarData, duration) { - if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return; - videoDuration = duration; - let speeds = []; - if (canSpeedData) { - speeds.push(...canSpeedData.map(d => parseFloat(d.speed))); - } - if (radarData && radarData.radarFrames) { - const egoSpeeds = radarData.radarFrames.map(frame => frame.egoVelocity[1] * 3.6); - speeds.push(...egoSpeeds); - } - - minSpeed = speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0; - maxSpeed = speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 10; - if (maxSpeed <= 0) maxSpeed = 10; - if (minSpeed >= 0) minSpeed = 0; - - drawStaticGraphToBuffer(canSpeedData, radarData); - p.redraw(); - }; - function drawStaticGraphToBuffer(canSpeedData, radarData) { + // This function is now attached to the p5 instance, making it public + // It's responsible for drawing the static background and data lines + p.drawStaticGraphToBuffer = function (canSpeedData, radarData) { const b = staticBuffer; b.clear(); const isDark = document.documentElement.classList.contains('dark'); @@ -758,69 +756,205 @@ b.noStroke(); b.text("Ego Speed", b.width - 95, pad.top + 30); b.pop(); - } - p.draw = function () { - if (document.documentElement.classList.contains('dark')) { - p.background(55, 65, 81); - } else { - p.background(255); + }; + + p.setup = function () { + let canvas = p.createCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight); + canvas.parent('speed-graph-container'); + staticBuffer = p.createGraphics(p.width, p.height); + p.noLoop(); + }; + + p.setData = function (canSpeedData, radarData, duration) { + if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return; + videoDuration = duration; + + let speeds = []; + if (canSpeedData) { + speeds.push(...canSpeedData.map(d => parseFloat(d.speed))); + } + if (radarData && radarData.radarFrames) { + const egoSpeeds = radarData.radarFrames.map(frame => frame.egoVelocity[1] * 3.6); + speeds.push(...egoSpeeds); } + + minSpeed = speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0; + maxSpeed = speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 10; + if (maxSpeed <= 0) maxSpeed = 10; + if (minSpeed >= 0) minSpeed = 0; + + p.drawStaticGraphToBuffer(canSpeedData, radarData); + p.redraw(); + }; + + p.draw = function () { if (!videoDuration) return; p.image(staticBuffer, 0, 0); drawTimeIndicator(); }; - function drawTimeIndicator() { const currentTime = videoPlayer.currentTime; const x = p.map(currentTime, 0, videoDuration, pad.left, p.width - pad.right); p.stroke(255, 0, 0, 150); p.strokeWeight(1.5); p.line(x, pad.top, x, p.height - pad.bottom); const videoAbsTimeMs = appState.videoStartDate.getTime() + (currentTime * 1000); const canIndex = findLastCanIndexBefore(videoAbsTimeMs, appState.canData); if (canIndex !== -1) { const canMsg = appState.canData[canIndex]; const y = p.map(canMsg.speed, minSpeed, maxSpeed, p.height - pad.bottom, pad.top); p.fill(255, 0, 0); p.noStroke(); p.ellipse(x, y, 8, 8); } } - p.windowResized = function () { p.resizeCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight); staticBuffer.resize(p.width, p.height); if ((appState.canData.length > 0 || appState.vizData) && videoDuration) { drawStaticGraphToBuffer(appState.canData, appState.vizData); } p.redraw(); }; + + function drawTimeIndicator() { + const currentTime = videoPlayer.currentTime; + const x = p.map(currentTime, 0, videoDuration, pad.left, p.width - pad.right); + p.stroke(255, 0, 0, 150); + p.strokeWeight(1.5); + p.line(x, pad.top, x, p.height - pad.bottom); + const videoAbsTimeMs = appState.videoStartDate.getTime() + (currentTime * 1000); + const canIndex = findLastCanIndexBefore(videoAbsTimeMs, appState.canData); + if (canIndex !== -1) { + const canMsg = appState.canData[canIndex]; + const y = p.map(canMsg.speed, minSpeed, maxSpeed, p.height - pad.bottom, pad.top); + p.fill(255, 0, 0); + p.noStroke(); + p.ellipse(x, y, 8, 8); + } + } + + p.windowResized = function () { + p.resizeCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight); + // Instead of resizing the buffer, we re-create it + staticBuffer = p.createGraphics(p.width, p.height); + // And we must re-draw the static content to the new buffer + if ((appState.canData.length > 0 || appState.vizData) && videoDuration) { + p.drawStaticGraphToBuffer(appState.canData, appState.vizData); + } + p.redraw(); + }; }; - function initializeVisualization(jsonString) { - try { - let cleanJsonString = jsonString.replace(/\b(Infinity|NaN|-Infinity)\b/gi, 'null'); - appState.vizData = JSON.parse(cleanJsonString); - if (!appState.vizData.radarFrames || appState.vizData.radarFrames.length === 0) { - showModal('Error: The JSON file does not contain any radar frames.'); + + + function setupVideoPlayer(fileURL) { videoPlayer.src = fileURL; videoPlayer.classList.remove('hidden'); videoPlaceholder.classList.add('hidden'); videoPlayer.playbackRate = parseFloat(speedSlider.value); } + 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(); } }); + 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 + + 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; } - const offsetMs = parseFloat(offsetInput.value) || 0; - appState.vizData.radarFrames.forEach(frame => { - frame.timestampMs = (appState.radarStartTimeMs + frame.timestamp) - appState.videoStartDate.getTime(); - }); - let snrValues = [], totalPoints = 0; - appState.vizData.radarFrames.forEach(frame => { - if (frame.pointCloud && frame.pointCloud.length > 0) { - totalPoints += frame.pointCloud.length; - frame.pointCloud.forEach(p => { - if (p.snr !== null) snrValues.push(p.snr); - }); - } - }); - if (totalPoints === 0) showModal('Warning: Loaded frames contain no point cloud data.'); - appState.globalMinSnr = snrValues.length > 0 ? Math.min(...snrValues) : 0; - appState.globalMaxSnr = snrValues.length > 0 ? Math.max(...snrValues) : 1; + + // 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(); + 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(sketch); - if (appState.speedGraphInstance && (appState.canData.length > 0 || appState.vizData)) { + + if (!appState.p5_instance) { + appState.p5_instance = new p5(sketch); + } + + 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(); } - } catch (error) { - showModal('Error parsing JSON file. Please check file format. Error: ' + error.message); - console.error("JSON Parsing Error:", error); + }; + reader.readAsText(file); + }); + 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; + } } - } - function setupVideoPlayer(fileURL) { videoPlayer.src = fileURL; videoPlayer.classList.remove('hidden'); videoPlaceholder.classList.add('hidden'); videoPlayer.playbackRate = parseFloat(speedSlider.value); } - function processCanLog(logContent) { if (!appState.videoStartDate) { showModal("Please load the video file first to synchronize the CAN log."); appState.rawCanLogText = logContent; return; } appState.canData = []; const lines = logContent.split('\n'); const logRegex = /(\d{2}):(\d{2}):(\d{2}):(\d{4})\s+Rx\s+\d+\s+0x([0-9a-fA-F]+)\s+s\s+\d+((?:\s+[0-9a-fA-F]{2})+)/; const canIdToDecode = '30F'; for (const line of lines) { const match = line.match(logRegex); if (match && match[5].toUpperCase() === canIdToDecode) { const [h, m, s, ms] = [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4].substring(0, 3))]; const msgDate = new Date(appState.videoStartDate); msgDate.setUTCHours(h, m, s, ms); const dataBytes = match[6].trim().split(/\s+/).map(hex => parseInt(hex, 16)); if (dataBytes.length >= 2) { const rawVal = (dataBytes[0] << 3) | (dataBytes[1] >> 5); const speed = (rawVal * 0.1).toFixed(1); appState.canData.push({ time: msgDate.getTime(), speed: speed }); } } } appState.canData.sort((a, b) => a.time - b.time); appState.rawCanLogText = null; console.log(`Processed ${appState.canData.length} CAN messages for ID ${canIdToDecode}.`); if (appState.canData.length > 0 || appState.vizData) { speedGraphPlaceholder.classList.add('hidden'); if (!appState.speedGraphInstance) { appState.speedGraphInstance = new p5(speedGraphSketch); } if (videoPlayer.duration) { appState.speedGraphInstance.setData(appState.canData, appState.vizData, videoPlayer.duration); } } else { showModal(`No CAN messages with ID 0x${canIdToDecode} found.`); } } - 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(); } }); - jsonFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; appState.jsonFilename = file.name; localStorage.setItem('appState.jsonFilename', appState.jsonFilename); calculateAndSetOffset(); const reader = new FileReader(); reader.onload = (e) => { saveFileToDB('json', e.target.result); initializeVisualization(e.target.result); }; reader.readAsText(file); }); - videoFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; appState.videoFilename = file.name; localStorage.setItem('appState.videoFilename', appState.videoFilename); saveFileToDB('video', file); calculateAndSetOffset(); if (appState.rawCanLogText) { processCanLog(appState.rawCanLogText); } const fileURL = URL.createObjectURL(file); setupVideoPlayer(fileURL); }); - canFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; appState.canLogFilename = file.name; localStorage.setItem('appState.canLogFilename', appState.canLogFilename); const reader = new FileReader(); reader.onload = (e) => { const logContent = e.target.result; saveFileToDB('canLogText', logContent); processCanLog(logContent); }; reader.readAsText(file); }); + + // 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); + } + }; + }); + + 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); + }); offsetInput.addEventListener('input', () => { autoOffsetIndicator.classList.add('hidden'); localStorage.setItem('visualizerOffset', offsetInput.value); }); 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(); } }); 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(); } }); @@ -886,62 +1020,93 @@ requestAnimationFrame(animationLoop); } - + // --- Application Initialization --- document.addEventListener('DOMContentLoaded', () => { - initializeTheme(); // Setup theme here as soon as DOM is loaded. + initializeTheme(); console.log("DEBUG: DOMContentLoaded fired. Starting session load."); + initDB(() => { console.log("DEBUG: Database initialized."); const savedOffset = localStorage.getItem('visualizerOffset'); if (savedOffset !== null) { offsetInput.value = savedOffset; } - appState.videoFilename = localStorage.getItem('appState.videoFilename'); - appState.jsonFilename = localStorage.getItem('appState.jsonFilename'); - appState.canLogFilename = localStorage.getItem('appState.canLogFilename'); - console.log(`DEBUG: Found filenames in localStorage: video='${appState.videoFilename}', json='${appState.jsonFilename}', can='${appState.canLogFilename}'`); + 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(); + const videoPromise = new Promise(resolve => loadFileFromDB('video', resolve)); const jsonPromise = new Promise(resolve => loadFileFromDB('json', resolve)); const canLogPromise = new Promise(resolve => loadFileFromDB('canLogText', resolve)); + Promise.all([videoPromise, jsonPromise, canLogPromise]).then(([videoBlob, jsonString, canLogText]) => { - console.log("DEBUG: All data fetched from IndexedDB. Proceeding with setup."); - const finalizeSetup = () => { - console.log("DEBUG: Finalizing setup."); - if (jsonString) { - console.log("DEBUG: Initializing visualization with JSON data."); - initializeVisualization(jsonString); - } else { - console.log("DEBUG: No JSON string found to initialize."); + console.log("DEBUG: All data fetched from IndexedDB."); + + const processAllData = () => { + console.log("DEBUG: Processing all loaded data."); + + // 1. Process JSON (only if we have a video date) + 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); + } } - if (canLogText) { - console.log("DEBUG: Processing CAN log."); - processCanLog(canLogText); - } else { - console.log("DEBUG: No CAN log text found to process."); + + // 2. Process CAN log (only if we have a video date) + 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(sketch); + } + } + 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); } }; + + // This is the main controller if (videoBlob) { - console.log("DEBUG: Video blob exists. Setting up player."); const fileURL = URL.createObjectURL(videoBlob); setupVideoPlayer(fileURL); - videoPlayer.onloadedmetadata = () => { - console.log("DEBUG: 'onloadedmetadata' event fired."); - finalizeSetup(); - }; + videoPlayer.onloadedmetadata = processAllData; // Process data ONLY when video is ready if (videoPlayer.readyState >= 1) { - console.log("DEBUG: Video was already ready (readyState >= 1). Manually calling final setup."); - finalizeSetup(); + processAllData(); // Or if it was ready immediately } } else { - console.log("DEBUG: No video blob. Calling final setup immediately."); - finalizeSetup(); + // If there's no video, there's nothing to process + console.log("DEBUG: No video blob found. Awaiting user input."); } }).catch(error => { console.error("DEBUG: Error during Promise.all data loading:", error); }); }); }); + + diff --git a/steps/src/fileParsers.js b/steps/src/fileParsers.js new file mode 100644 index 0000000..898d0c9 --- /dev/null +++ b/steps/src/fileParsers.js @@ -0,0 +1,88 @@ + +//--------------------CAN-LOG PARSER------------------------// + + +export function processCanLog(logContent, videoStartDate) { + // The function receives everything it needs as arguments. + // It no longer looks at the global state. + + if (!videoStartDate) { + // If the video isn't loaded, it can't do its job. + // It returns an object describing the problem. + return { error: "Please load the video file first to synchronize the CAN log.", rawCanLogText: logContent }; + } + + // This is a NEW, LOCAL variable, only for this function. + const canData = []; + const lines = logContent.split('\n'); + const logRegex = /(\d{2}):(\d{2}):(\d{2}):(\d{4})\s+Rx\s+\d+\s+0x([0-9a-fA-F]+)\s+s\s+\d+((?:\s+[0-9a-fA-F]{2})+)/; + const canIdToDecode = '30F'; + + for (const line of lines) { + const match = line.match(logRegex); + if (match && match[5].toUpperCase() === canIdToDecode) { + const [h, m, s, ms] = [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4].substring(0, 3))]; + const msgDate = new Date(videoStartDate); + msgDate.setUTCHours(h, m, s, ms); + const dataBytes = match[6].trim().split(/\s+/).map(hex => parseInt(hex, 16)); + if (dataBytes.length >= 2) { + const rawVal = (dataBytes[0] << 3) | (dataBytes[1] >> 5); + const speed = (rawVal * 0.1).toFixed(1); + canData.push({ time: msgDate.getTime(), speed: speed }); + } + } + } + // It sorts the LOCAL canData array. + canData.sort((a, b) => a.time - b.time); + + console.log(`Processed ${canData.length} CAN messages for ID ${canIdToDecode}.`); + + // It returns the finished product in a structured object. + return { data: canData }; +} + + +//--------------------JSON PARSER------------------------// + +// Add this new function to src/fileParsers.js + +export function parseVisualizationJson(jsonString, radarStartTimeMs, videoStartDate) { + try { + const cleanJsonString = jsonString.replace(/\b(Infinity|NaN|-Infinity)\b/gi, 'null'); + const vizData = JSON.parse(cleanJsonString); + + if (!vizData.radarFrames || vizData.radarFrames.length === 0) { + return { error: 'Error: The JSON file does not contain any radar frames.' }; + } + + // Perform timestamp calculations + vizData.radarFrames.forEach(frame => { + frame.timestampMs = (radarStartTimeMs + frame.timestamp) - videoStartDate.getTime(); + }); + + // Calculate SNR range from the data + let snrValues = [], totalPoints = 0; + vizData.radarFrames.forEach(frame => { + if (frame.pointCloud && frame.pointCloud.length > 0) { + totalPoints += frame.pointCloud.length; + frame.pointCloud.forEach(p => { + if (p.snr !== null) snrValues.push(p.snr); + }); + } + }); + + if (totalPoints === 0) { + console.warn('Warning: Loaded frames contain no point cloud data.'); + } + + const minSnr = snrValues.length > 0 ? Math.min(...snrValues) : 0; + const maxSnr = snrValues.length > 0 ? Math.max(...snrValues) : 1; + + // Return the finished data package + return { data: vizData, minSnr: minSnr, maxSnr: maxSnr }; + + } catch (error) { + console.error("JSON Parsing Error:", error); + return { error: 'Error parsing JSON file. Please check file format. Error: ' + error.message }; + } +} \ No newline at end of file diff --git a/steps/src/theme.js b/steps/src/theme.js index 9f65817..cae3052 100644 --- a/steps/src/theme.js +++ b/steps/src/theme.js @@ -1,8 +1,5 @@ import { appState } from './state.js'; - - - - +import { videoPlayer } from './dom.js'; // --- DARK MODE: Step 3 - Add the JavaScript Logic --- const themeToggleBtn = document.getElementById('theme-toggle'); const darkIcon = document.getElementById('theme-toggle-dark-icon'); @@ -20,15 +17,24 @@ function setTheme(theme) { lightIcon.classList.add('hidden'); localStorage.setItem('color-theme', 'light'); } + + // Redraw the main radar plot if (appState.p5_instance) appState.p5_instance.redraw(); + + // =================== THE FIX IS HERE =================== if (appState.speedGraphInstance) { + // 1. Check if there's data to draw. if ((appState.canData.length > 0 || appState.vizData) && videoPlayer.duration) { - appState.speedGraphInstance.setData(appState.canData, appState.vizData, videoPlayer.duration); + // 2. Force it to take a new "photograph" with the new theme colors. + appState.speedGraphInstance.drawStaticGraphToBuffer(appState.canData, appState.vizData); } + // 3. Display the new photograph. appState.speedGraphInstance.redraw(); } + // ================= END OF FIX ========================= } + export function initializeTheme() { const savedTheme = localStorage.getItem('color-theme'); if (savedTheme) {