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;
+}