diff --git a/steps/index.html b/steps/index.html index 72b98a1..b368f1d 100644 --- a/steps/index.html +++ b/steps/index.html @@ -272,7 +272,18 @@ RADAR_Y_MIN, RADAR_Y_MAX } from './src/constants.js'; - // --- Global State --- + + + // import utils and helpers from './src/utils.js'; + import { + findRadarFrameIndexForTime, + findLastCanIndexBefore, + extractTimestampInfo, + parseTimestamp + } from './src/utils.js'; + + + // --- Global State --- let vizData = null; let canData = []; let rawCanLogText = null; @@ -797,12 +808,10 @@ 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 = videoStartDate.getTime() + (currentTime * 1000); const canIndex = findLastCanIndexBefore(videoAbsTimeMs); if (canIndex !== -1) { const canMsg = 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); } } + 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 = videoStartDate.getTime() + (currentTime * 1000); const canIndex = findLastCanIndexBefore(videoAbsTimeMs, canData); if (canIndex !== -1) { const canMsg = 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 ((canData.length > 0 || vizData) && videoDuration) { drawStaticGraphToBuffer(canData, vizData); } p.redraw(); }; }; - function findRadarFrameIndexForTime(targetTimeMs) { if (!vizData || vizData.radarFrames.length === 0) return -1; let low = 0, high = vizData.radarFrames.length - 1, ans = 0; while (low <= high) { let mid = Math.floor((low + high) / 2); if (vizData.radarFrames[mid].timestampMs <= targetTimeMs) { ans = mid; low = mid + 1; } else { high = mid - 1; } } return ans; } - function findLastCanIndexBefore(targetTime) { if (!canData || canData.length === 0) return -1; let low = 0, high = canData.length - 1, ans = -1; while (low <= high) { let mid = Math.floor((low + high) / 2); if (canData[mid].time <= targetTime) { ans = mid; low = mid + 1; } else { high = mid - 1; } } return ans; } function initializeVisualization(jsonString) { try { let cleanJsonString = jsonString.replace(/\b(Infinity|NaN|-Infinity)\b/gi, 'null'); @@ -881,8 +890,6 @@ videoPlayer.addEventListener('ended', () => { isPlaying = false; playPauseBtn.textContent = 'Play'; }); document.addEventListener('keydown', (event) => { if (!vizData || ['ArrowRight', 'ArrowLeft'].indexOf(event.key) === -1) return; event.preventDefault(); if (isPlaying) { isPlaying = false; playPauseBtn.textContent = 'Play'; videoPlayer.pause(); } let newFrame = currentFrame; if (event.key === 'ArrowRight') newFrame = Math.min(vizData.radarFrames.length - 1, currentFrame + 1); else if (event.key === 'ArrowLeft') newFrame = Math.max(0, currentFrame - 1); if (newFrame !== currentFrame) { updateFrame(newFrame, true); mediaTimeStart = videoPlayer.currentTime; masterClockStart = performance.now(); } }); - function extractTimestampInfo(filename) { if (!filename) return null; let match = filename.match(/Tracks_(\d{8}_\d{6}\.\d{3})/); if (match) return { timestampStr: match[1], format: 'json' }; match = filename.match(/WIN_(\d{8})_(\d{2})_(\d{2})_(\d{2})/); if (match) { const timestamp = `${match[1]}_${match[2]}${match[3]}${match[4]}`; return { timestampStr: timestamp, format: 'video' }; } match = filename.match(/video_(\d{8}_\d{6})/); if (match) return { timestampStr: match[1], format: 'video' }; return null; } - function parseTimestamp(timestampStr, format) { if (!timestampStr || !format) return null; let day, month, year, hour, minute, second, millisecond = 0; if (format === 'video') { [year, month, day] = [timestampStr.substring(0, 4), timestampStr.substring(4, 6), timestampStr.substring(6, 8)];[hour, minute, second] = [timestampStr.substring(9, 11), timestampStr.substring(11, 13), timestampStr.substring(13, 15)]; } else if (format === 'json') { [day, month, year] = [timestampStr.substring(0, 2), timestampStr.substring(2, 4), timestampStr.substring(4, 8)];[hour, minute, second, millisecond] = [timestampStr.substring(9, 11), timestampStr.substring(11, 13), timestampStr.substring(13, 15), parseInt(timestampStr.substring(16, 19))]; } else { return null; } const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond)); return isNaN(date.getTime()) ? null : date; } function calculateAndSetOffset() { const jsonTimestampInfo = extractTimestampInfo(jsonFilename); const videoTimestampInfo = extractTimestampInfo(videoFilename); if (videoTimestampInfo) { videoStartDate = parseTimestamp(videoTimestampInfo.timestampStr, videoTimestampInfo.format); if (videoStartDate) console.log(`Video start date set to: ${videoStartDate.toISOString()}`); } if (jsonTimestampInfo) { const jsonDate = parseTimestamp(jsonTimestampInfo.timestampStr, jsonTimestampInfo.format); if (jsonDate) { radarStartTimeMs = jsonDate.getTime(); console.log(`Radar start date set to: ${jsonDate.toISOString()}`); if (videoStartDate) { const offset = radarStartTimeMs - videoStartDate.getTime(); offsetInput.value = offset; localStorage.setItem('visualizerOffset', offset); autoOffsetIndicator.classList.remove('hidden'); console.log(`Auto-calculated offset: ${offset} ms`); } } } } function animationLoop() { @@ -893,7 +900,7 @@ if (vizData && videoStartDate) { const offsetMs = parseFloat(offsetInput.value) || 0; const targetRadarTimeMs = (currentMediaTime * 1000) + offsetMs; - const targetFrame = findRadarFrameIndexForTime(targetRadarTimeMs); + const targetFrame = findRadarFrameIndexForTime(targetRadarTimeMs, vizData); if (targetFrame !== currentFrame) { updateFrame(targetFrame, false); } @@ -950,7 +957,7 @@ } function resetVisualization() { isPlaying = false; playPauseBtn.textContent = 'Play'; const numFrames = vizData.radarFrames.length; timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0; updateFrame(0, true); } - function updateCanDisplay(currentMediaTime) { if (canData.length > 0 && videoPlayer.src && videoStartDate) { const videoAbsoluteTimeMs = videoStartDate.getTime() + (currentMediaTime * 1000); const canIndex = findLastCanIndexBefore(videoAbsoluteTimeMs); if (canIndex !== -1) { const currentCanMessage = canData[canIndex]; canSpeedDisplay.textContent = `CAN: ${currentCanMessage.speed} km/h`; canSpeedDisplay.classList.remove('hidden'); } else { canSpeedDisplay.classList.add('hidden'); } } else { canSpeedDisplay.classList.add('hidden'); } } + function updateCanDisplay(currentMediaTime) { if (canData.length > 0 && videoPlayer.src && videoStartDate) { const videoAbsoluteTimeMs = videoStartDate.getTime() + (currentMediaTime * 1000); const canIndex = findLastCanIndexBefore(videoAbsoluteTimeMs, canData); if (canIndex !== -1) { const currentCanMessage = canData[canIndex]; canSpeedDisplay.textContent = `CAN: ${currentCanMessage.speed} km/h`; canSpeedDisplay.classList.remove('hidden'); } else { canSpeedDisplay.classList.add('hidden'); } } else { canSpeedDisplay.classList.add('hidden'); } } function updateDebugOverlay(currentMediaTime) { if (!toggleDebugOverlay.checked) { debugOverlay.classList.add('hidden'); @@ -964,7 +971,7 @@ content.push(`Video Frame: ${videoFrame}`); content.push(`Vid Abs Time: ${new Date(videoAbsoluteTimeMs).toISOString().split('T')[1].replace('Z', '')}`); if (canData.length > 0) { - const canIndex = findLastCanIndexBefore(videoAbsoluteTimeMs); + const canIndex = findLastCanIndexBefore(videoAbsoluteTimeMs, canData); if (canIndex !== -1) { const currentCanMessage = canData[canIndex]; content.push(`CAN Abs Time: ${new Date(currentCanMessage.time).toISOString().split('T')[1].replace('Z', '')}`); diff --git a/steps/src/utils.js b/steps/src/utils.js new file mode 100644 index 0000000..6376a34 --- /dev/null +++ b/steps/src/utils.js @@ -0,0 +1,63 @@ +export function findRadarFrameIndexForTime(targetTimeMs, vizData) { + if (!vizData || vizData.radarFrames.length === 0) return -1; + let low = 0, high = vizData.radarFrames.length - 1, ans = 0; + while (low <= high) { + let mid = Math.floor((low + high) / 2); + if (vizData.radarFrames[mid].timestampMs <= targetTimeMs) { + ans = mid; low = mid + 1; + } + else { + high = mid - 1; + } + } + return ans; +} + +export function findLastCanIndexBefore(targetTime, canData) { + if (!canData || canData.length === 0) return -1; + let low = 0, high = canData.length - 1, ans = -1; + while (low <= high) { + let mid = Math.floor((low + high) / 2); + if (canData[mid].time <= targetTime) { + ans = mid; low = mid + 1; + } else { + high = mid - 1; + + } + } + return ans; +} + +export function extractTimestampInfo(filename) { + if (!filename) return null; + let match = filename.match(/Tracks_(\d{8}_\d{6}\.\d{3})/); + if (match) return { timestampStr: match[1], format: 'json' }; + match = filename.match(/WIN_(\d{8})_(\d{2})_(\d{2})_(\d{2})/); + if (match) { + const timestamp = `${match[1]}_${match[2]}${match[3]}${match[4]}`; + return { timestampStr: timestamp, format: 'video' }; + } match = filename.match(/video_(\d{8}_\d{6})/); + if (match) return { + timestampStr: match[1], format: 'video' + }; + + return null; +} + +export function parseTimestamp(timestampStr, format) { + if (!timestampStr || !format) return null; + let day, month, year, hour, minute, second, millisecond = 0; + if (format === 'video') { + [year, month, day] = [timestampStr.substring(0, 4), timestampStr.substring(4, 6), timestampStr.substring(6, 8)]; + [hour, minute, second] = [timestampStr.substring(9, 11), timestampStr.substring(11, 13), timestampStr.substring(13, 15)]; + } + else if (format === 'json') { + [day, month, year] = [timestampStr.substring(0, 2), timestampStr.substring(2, 4), timestampStr.substring(4, 8)]; + [hour, minute, second, millisecond] = [timestampStr.substring(9, 11), timestampStr.substring(11, 13), timestampStr.substring(13, 15), parseInt(timestampStr.substring(16, 19))]; + } + else { + return null; + } + const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond)); + return isNaN(date.getTime()) ? null : date; +}