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) {