15 changed files with 1715 additions and 1194 deletions
-
492steps/index.html
-
53steps/readme.md
-
9steps/src/constants.js
-
108steps/src/db.js
-
72steps/src/dom.js
-
721steps/src/drawUtils.js
-
199steps/src/fileParsers.js
-
85steps/src/main.js
-
73steps/src/modal.js
-
264steps/src/p5/radarSketch.js
-
407steps/src/p5/speedGraphSketch.js
-
55steps/src/state.js
-
96steps/src/sync.js
-
92steps/src/theme.js
-
183steps/src/utils.js
@ -1,7 +1,12 @@ |
|||
// Maximum number of points to store for each object's trajectory.
|
|||
export const MAX_TRAJECTORY_LENGTH = 50; |
|||
// Frames per second for the video playback.
|
|||
export const VIDEO_FPS = 30; |
|||
|
|||
// Minimum X-coordinate for the radar plot in meters.
|
|||
export const RADAR_X_MIN = -20; |
|||
// Maximum X-coordinate for the radar plot in meters.
|
|||
export const RADAR_X_MAX = 20; |
|||
// Minimum Y-coordinate for the radar plot in meters.
|
|||
export const RADAR_Y_MIN = 0; |
|||
export const RADAR_Y_MAX = 60; |
|||
// Maximum Y-coordinate for the radar plot in meters.
|
|||
export const RADAR_Y_MAX = 60; |
|||
@ -1,55 +1,79 @@ |
|||
|
|||
|
|||
// --- IndexedDB for Caching --- //
|
|||
// -------------------------- IndexedDB for Caching ----------------- //
|
|||
let db; |
|||
|
|||
//---Initialize DB---//
|
|||
//---------------------------Initialize DB----------------------------//
|
|||
|
|||
// Initializes the IndexedDB database.
|
|||
// @param {function} callback - A function to be called once the database is initialized.
|
|||
export function initDB(callback) { |
|||
const request = indexedDB.open('visualizerDB', 1); |
|||
request.onupgradeneeded = function (event) { |
|||
const db = event.target.result; |
|||
if (!db.objectStoreNames.contains('files')) { |
|||
db.createObjectStore('files'); |
|||
} |
|||
}; |
|||
request.onsuccess = function (event) { |
|||
db = event.target.result; |
|||
console.log("Database initialized"); |
|||
if (callback) callback(); |
|||
}; request.onerror = function (event) { |
|||
console.error("IndexedDB error:", event.target.errorCode); |
|||
}; |
|||
// Open the database with the name "visualizerDB" and version 1.
|
|||
const request = indexedDB.open("visualizerDB", 1); |
|||
|
|||
// Event handler for when the database needs to be upgraded (e.g., first time creation or version change).
|
|||
request.onupgradeneeded = function (event) { |
|||
const db = event.target.result; |
|||
// Create an object store named "files" if it doesn't already exist.
|
|||
if (!db.objectStoreNames.contains("files")) { |
|||
db.createObjectStore("files"); |
|||
} |
|||
}; |
|||
|
|||
// Event handler for a successful database opening.
|
|||
request.onsuccess = function (event) { |
|||
db = event.target.result; |
|||
console.log("Database initialized"); |
|||
// Call the provided callback function.
|
|||
if (callback) callback(); |
|||
}; |
|||
|
|||
// Event handler for an error during database opening.
|
|||
request.onerror = function (event) { |
|||
console.error("IndexedDB error:", event.target.errorCode); |
|||
}; |
|||
} |
|||
|
|||
//---save file---//
|
|||
//---------------------------save file------------------------------//
|
|||
|
|||
// Saves a file (or any value) to the IndexedDB.
|
|||
// @param {string} key - The key to store the value under.
|
|||
// @param {*} value - The value to be stored.
|
|||
export function saveFileToDB(key, value) { |
|||
if (!db) return; |
|||
const transaction = db.transaction(['files'], 'readwrite'); |
|||
const store = transaction.objectStore('files'); |
|||
const request = store.put(value, key); |
|||
request.onsuccess = () => console.log(`File '${key}' saved to DB.`); |
|||
request.onerror = (event) => console.error(`Error saving file '${key}':`, event.target.error); |
|||
// If the database is not initialized, return.
|
|||
if (!db) return; |
|||
// Start a read-write transaction on the "files" object store.
|
|||
const transaction = db.transaction(["files"], "readwrite"); |
|||
const store = transaction.objectStore("files"); |
|||
// Put (add or update) the value with the given key.
|
|||
const request = store.put(value, key); |
|||
// Event handler for a successful save operation.
|
|||
request.onsuccess = () => console.log(`File '${key}' saved to DB.`); |
|||
// Event handler for an error during saving.
|
|||
request.onerror = (event) => |
|||
console.error(`Error saving file '${key}':`, event.target.error); |
|||
} |
|||
|
|||
//---load file---//
|
|||
//---------------------------load file--------------------------------//
|
|||
|
|||
export function loadFileFromDB(key, callback) { |
|||
if (!db) return; |
|||
const transaction = db.transaction(['files'], 'readonly'); |
|||
const store = transaction.objectStore('files'); const request = store.get(key); |
|||
request.onsuccess = function () { |
|||
if (request.result) { |
|||
callback(request.result); |
|||
} |
|||
else { |
|||
console.log(`File '${key}' not found in DB.`); |
|||
callback(null); |
|||
} |
|||
}; |
|||
request.onerror = (event) => { |
|||
console.error(`Error loading file '${key}':`, event.target.error); |
|||
callback(null); |
|||
}; |
|||
// If the database is not initialized, return.
|
|||
if (!db) return; |
|||
// Start a read-only transaction on the "files" object store.
|
|||
const transaction = db.transaction(["files"], "readonly"); |
|||
const store = transaction.objectStore("files"); |
|||
// Get the value associated with the given key.
|
|||
const request = store.get(key); |
|||
// Event handler for a successful retrieval.
|
|||
request.onsuccess = function () { |
|||
// If a result is found, call the callback with the result.
|
|||
if (request.result) { |
|||
callback(request.result); |
|||
} else { |
|||
console.log(`File '${key}' not found in DB.`); |
|||
callback(null); |
|||
} |
|||
}; // Event handler for an error during loading.
|
|||
request.onerror = (event) => { |
|||
console.error(`Error loading file '${key}':`, event.target.error); |
|||
callback(null); |
|||
}; |
|||
} |
|||
@ -1,88 +1,139 @@ |
|||
|
|||
//--------------------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 }; |
|||
// The function now receives all necessary data (logContent, videoStartDate) as arguments,
|
|||
// making it a pure function that doesn't rely on global state.
|
|||
if (!videoStartDate) { |
|||
// If videoStartDate is not provided, it means the video file hasn't been loaded yet.
|
|||
// The CAN log cannot be synchronized without it, so an error is returned.
|
|||
return { |
|||
// Error message to be displayed to the user.
|
|||
error: "Please load the video file first to synchronize the CAN log.", |
|||
// The raw log content is returned so it can be stored and processed later
|
|||
// once the videoStartDate becomes available.
|
|||
rawCanLogText: logContent, |
|||
}; |
|||
} |
|||
|
|||
// This is a NEW, LOCAL variable, only for this function.
|
|||
const canData = []; |
|||
const lines = logContent.split("\n"); |
|||
// Regular expression to parse CAN log lines.
|
|||
// It captures time components (HH:MM:SS:ms), CAN ID, and data bytes.
|
|||
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})+)/; |
|||
// The specific CAN ID (0x30F) we are interested in for speed data.
|
|||
const canIdToDecode = "30F"; |
|||
|
|||
for (const line of lines) { |
|||
const match = line.match(logRegex); |
|||
// Check if the line matches the regex and if the CAN ID is the one we want.
|
|||
if (match && match[5].toUpperCase() === canIdToDecode) { |
|||
// Extract time components from the regex match.
|
|||
const [h, m, s, ms] = [ |
|||
parseInt(match[1]), |
|||
parseInt(match[2]), |
|||
parseInt(match[3]), |
|||
parseInt(match[4].substring(0, 3)), |
|||
]; |
|||
// Create a Date object for the CAN message timestamp.
|
|||
// It uses the video's start date and then sets the time components from the log.
|
|||
const msgDate = new Date(videoStartDate); |
|||
msgDate.setUTCHours(h, m, s, ms); |
|||
// Extract and parse data bytes from the regex match.
|
|||
const dataBytes = match[6] |
|||
.trim() |
|||
.split(/\s+/) |
|||
.map((hex) => parseInt(hex, 16)); |
|||
// Check if there are enough data bytes to extract speed information.
|
|||
if (dataBytes.length >= 2) { |
|||
// Decode the raw speed value from the first two data bytes.
|
|||
// This specific decoding logic is based on the CAN message format.
|
|||
const rawVal = (dataBytes[0] << 3) | (dataBytes[1] >> 5); |
|||
// Convert the raw value to km/h and format it to one decimal place.
|
|||
const speed = (rawVal * 0.1).toFixed(1); |
|||
canData.push({ time: msgDate.getTime(), speed: speed }); |
|||
} |
|||
} |
|||
} |
|||
// Sort the processed CAN data points by their timestamp.
|
|||
canData.sort((a, b) => a.time - b.time); |
|||
|
|||
// 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}.` |
|||
); |
|||
|
|||
console.log(`Processed ${canData.length} CAN messages for ID ${canIdToDecode}.`); |
|||
|
|||
// It returns the finished product in a structured object.
|
|||
return { data: canData }; |
|||
// It returns the finished product in a structured object.
|
|||
// The processed CAN data is returned under the 'data' key.
|
|||
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(); |
|||
}); |
|||
export function parseVisualizationJson( |
|||
jsonString, |
|||
radarStartTimeMs, |
|||
videoStartDate |
|||
) { |
|||
try { |
|||
// Replace Infinity, NaN, and -Infinity with "null" to prevent JSON.parse errors.
|
|||
const cleanJsonString = jsonString.replace( |
|||
/\b(Infinity|NaN|-Infinity)\b/gi, |
|||
"null" |
|||
); |
|||
// Parse the cleaned JSON string into a JavaScript object.
|
|||
const vizData = JSON.parse(cleanJsonString); |
|||
|
|||
// Validate if the parsed data contains radar frames.
|
|||
if (!vizData.radarFrames || vizData.radarFrames.length === 0) { |
|||
return { |
|||
error: "Error: The JSON file does not contain any radar frames.", |
|||
}; |
|||
} |
|||
|
|||
// 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); |
|||
}); |
|||
} |
|||
// Perform timestamp calculations for each radar frame.
|
|||
// The `timestampMs` for each frame is calculated relative to the video's start time,
|
|||
// taking into account the `radarStartTimeMs` (extracted from JSON filename)
|
|||
// and the `videoStartDate` (extracted from video filename).
|
|||
// This ensures synchronization between radar data and video.
|
|||
vizData.radarFrames.forEach((frame) => { |
|||
frame.timestampMs = |
|||
radarStartTimeMs + frame.timestamp - videoStartDate.getTime(); |
|||
}); |
|||
|
|||
// Calculate SNR range from the data
|
|||
let snrValues = [], |
|||
totalPoints = 0; // Counter for total points across all frames.
|
|||
vizData.radarFrames.forEach((frame) => { |
|||
if (frame.pointCloud && frame.pointCloud.length > 0) { |
|||
totalPoints += frame.pointCloud.length; |
|||
frame.pointCloud.forEach((p) => { |
|||
// Collect SNR values, ignoring nulls.
|
|||
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 }; |
|||
// Warn if no point cloud data was found in the loaded frames.
|
|||
if (totalPoints === 0) { |
|||
console.warn("Warning: Loaded frames contain no point cloud data."); |
|||
} |
|||
} |
|||
|
|||
// Determine the global minimum and maximum SNR values from the collected data.
|
|||
// These values are used for scaling the SNR color legend.
|
|||
// Default to 0 and 1 if no SNR values are found to prevent errors.
|
|||
const minSnr = snrValues.length > 0 ? Math.min(...snrValues) : 0; |
|||
const maxSnr = snrValues.length > 0 ? Math.max(...snrValues) : 1; |
|||
|
|||
// Return the finished data package
|
|||
// This object contains the processed visualization data, and the calculated min/max SNR.
|
|||
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, |
|||
}; |
|||
} |
|||
} |
|||
@ -1,33 +1,46 @@ |
|||
|
|||
import { modalText, modalCancelBtn, modalContainer, modalOverlay, modalContent, modalOkBtn } from './dom.js'; |
|||
import { |
|||
modalText, |
|||
modalCancelBtn, |
|||
modalContainer, |
|||
modalOverlay, |
|||
modalContent, |
|||
modalOkBtn, |
|||
} from "./dom.js"; |
|||
|
|||
// --- Custom Modal Logic --- //
|
|||
let modalResolve = null; |
|||
export function showModal(message, isConfirm = false) { |
|||
return new Promise(resolve => { |
|||
modalText.textContent = message; |
|||
modalCancelBtn.classList.toggle('hidden', !isConfirm); |
|||
modalContainer.classList.remove('hidden'); |
|||
setTimeout(() => { |
|||
modalOverlay.classList.remove('opacity-0'); |
|||
modalContent.classList.remove('scale-95'); |
|||
} |
|||
, 10); |
|||
modalResolve = resolve; |
|||
}); |
|||
} |
|||
function hideModal(value) { |
|||
modalOverlay.classList.add('opacity-0'); |
|||
modalContent.classList.add('scale-95'); |
|||
setTimeout(() => { |
|||
modalContainer.classList.add('hidden'); |
|||
if (modalResolve) modalResolve(value); |
|||
}, 200); |
|||
} |
|||
|
|||
|
|||
//----------------------Modal Event Listeners----------------------//
|
|||
// 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) { |
|||
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); |
|||
// Make the modal container visible.
|
|||
modalContainer.classList.remove("hidden"); |
|||
// Add a slight delay for CSS transitions to take effect, making the modal appear smoothly.
|
|||
setTimeout(() => { |
|||
modalOverlay.classList.remove("opacity-0"); |
|||
modalContent.classList.remove("scale-95"); |
|||
}, 10); |
|||
// Store the resolve function to be called when the modal is closed.
|
|||
modalResolve = resolve; |
|||
}); |
|||
} |
|||
// 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"); |
|||
if (modalResolve) modalResolve(value); |
|||
}, 200); |
|||
} |
|||
|
|||
modalOkBtn.addEventListener('click', () => hideModal(true)); |
|||
modalCancelBtn.addEventListener('click', () => hideModal(false)); |
|||
modalOverlay.addEventListener('click', () => hideModal(false)); |
|||
//----------------------Modal Event Listeners----------------------//
|
|||
// Event listener for the "OK" button. Resolves the modal Promise with 'true'.
|
|||
modalOkBtn.addEventListener("click", () => hideModal(true)); |
|||
// Event listener for the "Cancel" button. Resolves the modal Promise with 'false'.
|
|||
modalCancelBtn.addEventListener("click", () => hideModal(false)); |
|||
// Event listener for clicking on the modal overlay (outside the content). Resolves the modal Promise with 'false'.
|
|||
modalOverlay.addEventListener("click", () => hideModal(false)); |
|||
@ -1,126 +1,170 @@ |
|||
import { appState } from "../state.js"; |
|||
import { |
|||
appState |
|||
} from '../state.js'; |
|||
RADAR_X_MAX, |
|||
// Define radar plot boundaries
|
|||
RADAR_X_MIN, |
|||
RADAR_Y_MAX, |
|||
RADAR_Y_MIN, |
|||
} from "../constants.js"; |
|||
import { canvasContainer, toggleSnrColor, toggleTracks } from "../dom.js"; |
|||
import { |
|||
RADAR_X_MAX, |
|||
RADAR_X_MIN, |
|||
RADAR_Y_MAX, |
|||
RADAR_Y_MIN |
|||
} from '../constants.js'; |
|||
import { |
|||
canvasContainer, |
|||
toggleSnrColor, |
|||
toggleTracks |
|||
} from '../dom.js'; |
|||
import { |
|||
drawStaticRegionsToBuffer, |
|||
drawAxes, |
|||
drawPointCloud, |
|||
drawTrajectories, |
|||
drawTrackMarkers, |
|||
snrColors, |
|||
handleCloseUpDisplay // BUG FIX 1: Import the close-up handler
|
|||
} from '../drawUtils.js'; |
|||
drawStaticRegionsToBuffer, |
|||
drawAxes, |
|||
drawPointCloud, |
|||
// Import drawing utility functions
|
|||
drawTrajectories, |
|||
drawTrackMarkers, |
|||
snrColors, |
|||
handleCloseUpDisplay, // BUG FIX 1: Import the close-up handler
|
|||
} from "../drawUtils.js"; |
|||
|
|||
export const radarSketch = function(p) { |
|||
let plotScales = { |
|||
plotScaleX: 1, |
|||
plotScaleY: 1 |
|||
}; |
|||
let staticBackgroundBuffer, snrLegendBuffer; |
|||
|
|||
function calculatePlotScales() { |
|||
const hPad = 0.05, |
|||
vPad = 0.05, |
|||
bOff = 0.05; |
|||
const aW = p.width * (1 - 2 * hPad); |
|||
const aH = p.height * (1 - bOff - vPad); |
|||
plotScales.plotScaleX = aW / (RADAR_X_MAX - RADAR_X_MIN); |
|||
plotScales.plotScaleY = aH / (RADAR_Y_MAX - RADAR_Y_MIN); |
|||
} |
|||
export const radarSketch = function (p) { |
|||
// Object to store calculated plot scales
|
|||
let plotScales = { |
|||
plotScaleX: 1, |
|||
plotScaleY: 1, |
|||
}; |
|||
// p5.Graphics buffers for static elements to optimize drawing
|
|||
let staticBackgroundBuffer, snrLegendBuffer; |
|||
|
|||
p.setup = function() { |
|||
let canvas = p.createCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight); |
|||
canvas.parent('canvas-container'); |
|||
staticBackgroundBuffer = p.createGraphics(p.width, p.height); |
|||
snrLegendBuffer = p.createGraphics(100, 450); |
|||
// Function to calculate scaling factors for radar coordinates to canvas pixels
|
|||
function calculatePlotScales() { |
|||
// Padding and offset values for the plot area
|
|||
const hPad = 0.05, |
|||
vPad = 0.05, |
|||
bOff = 0.05; |
|||
// Calculate available width and height for the plot
|
|||
const aW = p.width * (1 - 2 * hPad); |
|||
const aH = p.height * (1 - bOff - vPad); |
|||
// Determine plot scales based on radar boundaries and available canvas space
|
|||
plotScales.plotScaleX = aW / (RADAR_X_MAX - RADAR_X_MIN); |
|||
plotScales.plotScaleY = aH / (RADAR_Y_MAX - RADAR_Y_MIN); |
|||
} |
|||
|
|||
calculatePlotScales(); |
|||
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr); |
|||
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales); |
|||
p.noLoop(); |
|||
}; |
|||
p.setup = function () { |
|||
// Create the p5.js canvas and attach it to the specified DOM element
|
|||
let canvas = p.createCanvas( |
|||
canvasContainer.offsetWidth, |
|||
canvasContainer.offsetHeight |
|||
); |
|||
canvas.parent("canvas-container"); |
|||
// Initialize graphics buffers
|
|||
staticBackgroundBuffer = p.createGraphics(p.width, p.height); |
|||
snrLegendBuffer = p.createGraphics(100, 450); |
|||
|
|||
p.draw = function() { |
|||
p.background(document.documentElement.classList.contains('dark') ? p.color(55, 65, 81) : 255); |
|||
if (!appState.vizData) return; |
|||
calculatePlotScales(); |
|||
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr); |
|||
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales); |
|||
p.noLoop(); |
|||
// Disable continuous looping, redraw will be called manually
|
|||
}; |
|||
|
|||
p.image(staticBackgroundBuffer, 0, 0); |
|||
p.draw = function () { |
|||
// Set background color based on current theme (dark/light)
|
|||
p.background( |
|||
document.documentElement.classList.contains("dark") |
|||
? p.color(55, 65, 81) |
|||
: 255 |
|||
); |
|||
// If no visualization data is loaded, stop drawing
|
|||
if (!appState.vizData) return; |
|||
|
|||
p.push(); |
|||
p.translate(p.width / 2, p.height * 0.95); |
|||
p.scale(1, -1); |
|||
// Draw the pre-rendered static background elements
|
|||
p.image(staticBackgroundBuffer, 0, 0); |
|||
|
|||
calculatePlotScales(); |
|||
drawAxes(p, plotScales); |
|||
// Apply transformations for radar coordinate system (origin at bottom-center, Y-axis inverted)
|
|||
p.push(); |
|||
p.translate(p.width / 2, p.height * 0.95); |
|||
p.scale(1, -1); |
|||
|
|||
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
|||
if (frameData) { |
|||
if (toggleTracks.checked) { |
|||
drawTrajectories(p, plotScales); |
|||
drawTrackMarkers(p, plotScales); |
|||
} |
|||
drawPointCloud(p, frameData.pointCloud, plotScales); |
|||
} |
|||
p.pop(); |
|||
// Recalculate plot scales (important for window resizing)
|
|||
calculatePlotScales(); |
|||
// Draw coordinate axes
|
|||
drawAxes(p, plotScales); |
|||
|
|||
// BUG FIX 1: Call the close-up handler if the mode is active
|
|||
if (appState.isCloseUpMode) { |
|||
handleCloseUpDisplay(p, plotScales); |
|||
} |
|||
// Get current frame data
|
|||
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
|||
if (frameData) { |
|||
// Draw object trajectories and markers if enabled
|
|||
if (toggleTracks.checked) { |
|||
drawTrajectories(p, plotScales); |
|||
drawTrackMarkers(p, plotScales); |
|||
} |
|||
// Draw the point cloud for the current frame
|
|||
drawPointCloud(p, frameData.pointCloud, plotScales); |
|||
} |
|||
p.pop(); |
|||
|
|||
if (toggleSnrColor.checked) { |
|||
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10); |
|||
} |
|||
}; |
|||
// BUG FIX 1: Call the close-up handler if the mode is active
|
|||
if (appState.isCloseUpMode) { |
|||
handleCloseUpDisplay(p, plotScales); |
|||
} |
|||
|
|||
p.drawSnrLegendToBuffer = function(minV, maxV) { |
|||
const b = snrLegendBuffer; |
|||
const localSnrColors = snrColors(p); |
|||
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(localSnrColors.c1, localSnrColors.c2, amt / 0.25); |
|||
else if (amt < 0.5) c = b.lerpColor(localSnrColors.c2, localSnrColors.c3, (amt - 0.25) / 0.25); |
|||
else if (amt < 0.75) c = b.lerpColor(localSnrColors.c3, localSnrColors.c4, (amt - 0.5) / 0.25); |
|||
else c = b.lerpColor(localSnrColors.c4, localSnrColors.c5, (amt - 0.75) / 0.25); |
|||
b.stroke(c); |
|||
b.line(lx, ly + i, lx + lw, ly + i); |
|||
} |
|||
b.fill(document.documentElement.classList.contains('dark') ? 255 : 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(); |
|||
}; |
|||
// Draw the SNR legend if enabled
|
|||
if (toggleSnrColor.checked) { |
|||
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10); |
|||
} |
|||
}; |
|||
|
|||
// Function to draw the SNR legend to its buffer
|
|||
p.drawSnrLegendToBuffer = function (minV, maxV) { |
|||
// Reference to the SNR legend buffer
|
|||
const b = snrLegendBuffer; |
|||
const localSnrColors = snrColors(p); |
|||
b.clear(); |
|||
b.push(); |
|||
const lx = 10, |
|||
ly = 20, |
|||
lw = 15, |
|||
// Dimensions for the color bar
|
|||
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(localSnrColors.c1, localSnrColors.c2, amt / 0.25); |
|||
else if (amt < 0.5) |
|||
c = b.lerpColor( |
|||
localSnrColors.c2, |
|||
localSnrColors.c3, |
|||
(amt - 0.25) / 0.25 |
|||
); |
|||
else if (amt < 0.75) |
|||
c = b.lerpColor( |
|||
localSnrColors.c3, |
|||
localSnrColors.c4, |
|||
(amt - 0.5) / 0.25 |
|||
); |
|||
else |
|||
c = b.lerpColor( |
|||
localSnrColors.c4, |
|||
localSnrColors.c5, |
|||
// Interpolate colors based on position
|
|||
(amt - 0.75) / 0.25 |
|||
); |
|||
b.stroke(c); |
|||
b.line(lx, ly + i, lx + lw, ly + i); |
|||
} |
|||
// Set text color based on theme
|
|||
b.fill(document.documentElement.classList.contains("dark") ? 255 : 0); |
|||
b.noStroke(); |
|||
b.textSize(10); |
|||
b.textAlign(b.LEFT, b.CENTER); |
|||
// Draw min/max SNR values and label
|
|||
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); |
|||
// BUG FIX 2: Re-create the buffer instead of resizing it
|
|||
staticBackgroundBuffer = p.createGraphics(p.width, p.height); |
|||
calculatePlotScales(); |
|||
// Re-draw the static content to the new buffer
|
|||
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales); |
|||
if (appState.vizData) p.redraw(); |
|||
}; |
|||
// Handle window resizing event
|
|||
p.windowResized = function () { |
|||
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight); |
|||
// BUG FIX 2: Re-create the buffer instead of resizing it
|
|||
staticBackgroundBuffer = p.createGraphics(p.width, p.height); |
|||
calculatePlotScales(); |
|||
// Re-draw the static content to the new buffer
|
|||
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales); |
|||
if (appState.vizData) p.redraw(); |
|||
}; |
|||
}; |
|||
@ -1,170 +1,265 @@ |
|||
//---Import APPSTATE VIDEOPLAYER and FindLastCanIndex---//
|
|||
|
|||
import { appState |
|||
|
|||
} from '../state.js'; |
|||
|
|||
import { videoPlayer, speedGraphContainer |
|||
|
|||
} from '../dom.js'; |
|||
|
|||
import { findLastCanIndexBefore |
|||
|
|||
} from '../utils.js'; |
|||
|
|||
import { appState } from "../state.js"; |
|||
import { videoPlayer, speedGraphContainer } from "../dom.js"; |
|||
import { findLastCanIndexBefore } from "../utils.js"; |
|||
|
|||
export const speedGraphSketch = function (p) { |
|||
let staticBuffer, minSpeed, maxSpeed, videoDuration; |
|||
const pad = { top: 20, right: 130, bottom: 30, left: 50 }; |
|||
|
|||
// 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'); |
|||
b.background(isDark ? [55, 65, 81] : 255); |
|||
const gridColor = isDark ? 100 : 200; |
|||
const textColor = isDark ? 200 : 100; |
|||
|
|||
b.push(); |
|||
b.stroke(gridColor); |
|||
b.strokeWeight(1); |
|||
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom); |
|||
b.line(pad.left, b.height - pad.bottom, b.width - pad.right, b.height - pad.bottom); |
|||
b.textAlign(b.RIGHT, b.CENTER); |
|||
b.noStroke(); |
|||
b.fill(textColor); |
|||
b.textSize(10); |
|||
for (let s = minSpeed; s <= maxSpeed; s += 10) { |
|||
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|||
b.text(s, pad.left - 8, y); |
|||
if (s === 0) { |
|||
b.strokeWeight(1.5); |
|||
b.stroke(isDark ? 150 : 180); |
|||
} else { |
|||
b.strokeWeight(1); |
|||
b.stroke(isDark ? 80 : 230); |
|||
} |
|||
b.line(pad.left + 1, y, b.width - pad.right, y); |
|||
b.noStroke(); |
|||
} |
|||
|
|||
b.fill(textColor); |
|||
b.text("km/h", pad.left - 8, pad.top - 8); |
|||
b.textAlign(b.CENTER, b.TOP); |
|||
b.noStroke(); |
|||
b.fill(isDark ? 180 : 150); |
|||
const tInt = Math.max(1, Math.floor(videoDuration / 10)); |
|||
for (let t = 0; t <= videoDuration; t += tInt) { const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right); b.text(Math.round(t), x, b.height - pad.bottom + 5); } |
|||
b.fill(textColor); |
|||
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18); |
|||
b.pop(); |
|||
|
|||
if (canSpeedData && canSpeedData.length > 0) { |
|||
b.noFill(); |
|||
b.stroke(0, 150, 255); |
|||
b.strokeWeight(1.5); |
|||
b.beginShape(); |
|||
for (const d of canSpeedData) { const relTime = (d.time - appState.videoStartDate.getTime()) / 1000; if (relTime >= 0 && relTime <= videoDuration) { const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); const y = b.map(d.speed, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); b.vertex(x, y); } } |
|||
b.endShape(); |
|||
} |
|||
// Declare variables for the static buffer, min/max speed for scaling, and video duration.
|
|||
let staticBuffer, minSpeed, maxSpeed, videoDuration; |
|||
// Define padding for the graph to ensure elements are not drawn at the edges.
|
|||
const pad = { top: 20, right: 130, bottom: 30, left: 50 }; |
|||
|
|||
if (radarData && radarData.radarFrames) { |
|||
b.stroke(0, 200, 100); |
|||
b.drawingContext.setLineDash([5, 5]); |
|||
b.beginShape(); |
|||
for (const frame of radarData.radarFrames) { |
|||
const relTime = frame.timestampMs / 1000; |
|||
if (relTime >= 0 && relTime <= videoDuration) { |
|||
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); |
|||
const egoSpeedKmh = frame.egoVelocity[1] * 3.6; |
|||
const y = b.map(egoSpeedKmh, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|||
b.vertex(x, y); |
|||
} |
|||
} |
|||
b.endShape(); |
|||
b.drawingContext.setLineDash([]); |
|||
} |
|||
/** |
|||
* Draws the static elements of the speed graph (axes, grid, labels, and data lines) |
|||
* to an off-screen buffer. This optimizes performance by not redrawing these elements |
|||
* every frame. |
|||
* @param {Array} canSpeedData - Array of CAN speed data points. |
|||
* @param {Object} radarData - Object containing radar frames with ego velocity. |
|||
*/ |
|||
// 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"); |
|||
b.background(isDark ? [55, 65, 81] : 255); |
|||
const gridColor = isDark ? 100 : 200; |
|||
const textColor = isDark ? 200 : 100; // Determine text color based on theme.
|
|||
|
|||
b.push(); |
|||
b.strokeWeight(2); |
|||
b.noStroke(); |
|||
b.fill(textColor); |
|||
b.textAlign(b.LEFT, b.CENTER); |
|||
b.stroke(0, 150, 255); |
|||
b.line(b.width - 120, pad.top + 10, b.width - 100, pad.top + 10); |
|||
b.noStroke(); |
|||
b.text("CAN Speed", b.width - 95, pad.top + 10); |
|||
b.stroke(0, 200, 100); |
|||
b.drawingContext.setLineDash([3, 3]); |
|||
b.line(b.width - 120, pad.top + 30, b.width - 100, pad.top + 30); |
|||
b.drawingContext.setLineDash([]); |
|||
b.noStroke(); |
|||
b.text("Ego Speed", b.width - 95, pad.top + 30); |
|||
b.pop(); |
|||
}; |
|||
// Push current drawing style settings onto a stack.
|
|||
b.push(); |
|||
// Set stroke for grid lines.
|
|||
b.stroke(gridColor); |
|||
// Set stroke weight for grid lines.
|
|||
b.strokeWeight(1); |
|||
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom); |
|||
b.line( |
|||
pad.left, |
|||
b.height - pad.bottom, |
|||
b.width - pad.right, |
|||
b.height - pad.bottom |
|||
); // Draw Y and X axes.
|
|||
// Set text alignment for Y-axis labels.
|
|||
b.textAlign(b.RIGHT, b.CENTER); |
|||
b.noStroke(); |
|||
b.fill(textColor); |
|||
// Set text size for labels.
|
|||
b.textSize(10); |
|||
// Draw horizontal grid lines and speed labels.
|
|||
for (let s = minSpeed; s <= maxSpeed; s += 10) { |
|||
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|||
b.text(s, pad.left - 8, y); |
|||
if (s === 0) { |
|||
b.strokeWeight(1.5); |
|||
b.stroke(isDark ? 150 : 180); |
|||
} else { |
|||
b.strokeWeight(1); |
|||
b.stroke(isDark ? 80 : 230); |
|||
} |
|||
b.line(pad.left + 1, y, b.width - pad.right, y); |
|||
b.noStroke(); |
|||
} |
|||
// Draw Y-axis unit label.
|
|||
b.fill(textColor); |
|||
b.text("km/h", pad.left - 8, pad.top - 8); |
|||
// Set text alignment for X-axis labels.
|
|||
b.textAlign(b.CENTER, b.TOP); |
|||
b.noStroke(); |
|||
b.fill(isDark ? 180 : 150); |
|||
// Calculate time interval for X-axis labels.
|
|||
const tInt = Math.max(1, Math.floor(videoDuration / 10)); |
|||
for (let t = 0; t <= videoDuration; t += tInt) { |
|||
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right); |
|||
b.text(Math.round(t), x, b.height - pad.bottom + 5); |
|||
} |
|||
b.fill(textColor); |
|||
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18); |
|||
// Restore previous drawing style settings.
|
|||
b.pop(); |
|||
|
|||
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(); |
|||
}; |
|||
// Draw CAN speed data line if available.
|
|||
if (canSpeedData && canSpeedData.length > 0) { |
|||
b.noFill(); // Do not fill the shape.
|
|||
b.stroke(0, 150, 255); |
|||
b.strokeWeight(1.5); |
|||
b.beginShape(); |
|||
for (const d of canSpeedData) { |
|||
const relTime = (d.time - appState.videoStartDate.getTime()) / 1000; |
|||
if (relTime >= 0 && relTime <= videoDuration) { |
|||
const x = b.map( |
|||
relTime, |
|||
0, |
|||
videoDuration, |
|||
pad.left, |
|||
b.width - pad.right |
|||
); |
|||
const y = b.map( |
|||
d.speed, |
|||
minSpeed, |
|||
maxSpeed, |
|||
b.height - pad.bottom, |
|||
pad.top |
|||
); |
|||
b.vertex(x, y); |
|||
} |
|||
} |
|||
b.endShape(); |
|||
} // End of CAN speed data drawing.
|
|||
|
|||
p.setData = function (canSpeedData, radarData, duration) { |
|||
if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return; |
|||
videoDuration = duration; |
|||
// Draw radar ego speed data line if available.
|
|||
if (radarData && radarData.radarFrames) { |
|||
b.stroke(0, 200, 100); |
|||
b.drawingContext.setLineDash([5, 5]); |
|||
b.beginShape(); |
|||
for (const frame of radarData.radarFrames) { |
|||
const relTime = frame.timestampMs / 1000; |
|||
if (relTime >= 0 && relTime <= videoDuration) { |
|||
const x = b.map( |
|||
relTime, |
|||
0, |
|||
videoDuration, |
|||
pad.left, |
|||
b.width - pad.right |
|||
); |
|||
const egoSpeedKmh = frame.egoVelocity[1] * 3.6; |
|||
const y = b.map( |
|||
egoSpeedKmh, |
|||
minSpeed, |
|||
maxSpeed, |
|||
b.height - pad.bottom, |
|||
pad.top |
|||
); |
|||
b.vertex(x, y); |
|||
} |
|||
} |
|||
b.endShape(); |
|||
b.drawingContext.setLineDash([]); // Reset line dash to solid.
|
|||
} // End of radar ego speed data drawing.
|
|||
|
|||
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); |
|||
} |
|||
// Draw legend for the graph lines.
|
|||
b.push(); |
|||
b.strokeWeight(2); |
|||
b.noStroke(); |
|||
b.fill(textColor); |
|||
b.textAlign(b.LEFT, b.CENTER); |
|||
b.stroke(0, 150, 255); |
|||
b.line(b.width - 120, pad.top + 10, b.width - 100, pad.top + 10); |
|||
b.noStroke(); |
|||
b.text("CAN Speed", b.width - 95, pad.top + 10); |
|||
b.stroke(0, 200, 100); |
|||
b.drawingContext.setLineDash([3, 3]); |
|||
b.line(b.width - 120, pad.top + 30, b.width - 100, pad.top + 30); |
|||
b.drawingContext.setLineDash([]); |
|||
b.noStroke(); |
|||
b.text("Ego Speed", b.width - 95, pad.top + 30); |
|||
b.pop(); |
|||
}; |
|||
/** |
|||
* p5.js setup function. Initializes the canvas and static buffer. |
|||
*/ |
|||
p.setup = function () { |
|||
let canvas = p.createCanvas( |
|||
speedGraphContainer.offsetWidth, |
|||
speedGraphContainer.offsetHeight |
|||
); |
|||
canvas.parent("speed-graph-container"); |
|||
// Create an off-screen graphics buffer for static elements.
|
|||
staticBuffer = p.createGraphics(p.width, p.height); |
|||
// Disable continuous looping; draw will be called manually.
|
|||
p.noLoop(); |
|||
}; |
|||
/** |
|||
* Sets the data for the speed graph and recalculates min/max speed for scaling. |
|||
* @param {Array} canSpeedData - Array of CAN speed data points. |
|||
* @param {Object} radarData - Object containing radar frames with ego velocity. |
|||
* @param {number} duration - The total duration of the video in seconds. |
|||
*/ |
|||
p.setData = function (canSpeedData, radarData, duration) { |
|||
if ((!canSpeedData || canSpeedData.length === 0) && !radarData) return; // Exit if no data.
|
|||
videoDuration = duration; |
|||
|
|||
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; |
|||
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); |
|||
} |
|||
|
|||
p.drawStaticGraphToBuffer(canSpeedData, radarData); |
|||
p.redraw(); |
|||
}; |
|||
// Calculate min and max speeds for Y-axis scaling, rounding to nearest 10.
|
|||
minSpeed = |
|||
speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0; |
|||
maxSpeed = |
|||
speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 10; |
|||
// Ensure maxSpeed is at least 10 if all speeds are non-positive.
|
|||
if (maxSpeed <= 0) maxSpeed = 10; |
|||
// Ensure minSpeed is 0 if all speeds are non-negative.
|
|||
if (minSpeed >= 0) minSpeed = 0; |
|||
|
|||
p.draw = function () { |
|||
if (!videoDuration) return; |
|||
p.image(staticBuffer, 0, 0); |
|||
drawTimeIndicator(); |
|||
}; |
|||
// Redraw the static graph elements to the buffer with new data.
|
|||
p.drawStaticGraphToBuffer(canSpeedData, radarData); |
|||
// Request a redraw of the main canvas.
|
|||
p.redraw(); |
|||
}; |
|||
/** |
|||
* p5.js draw function. Draws the static buffer and the dynamic time indicator. |
|||
*/ |
|||
p.draw = function () { |
|||
if (!videoDuration) return; // Only draw if video duration is set.
|
|||
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); |
|||
} |
|||
} |
|||
function drawTimeIndicator() { |
|||
const currentTime = videoPlayer.currentTime; |
|||
const x = p.map( |
|||
currentTime, |
|||
0, |
|||
videoDuration, |
|||
pad.left, |
|||
p.width - pad.right |
|||
); // Map current time to X-coordinate.
|
|||
// Draw the red time indicator line.
|
|||
p.stroke(255, 0, 0, 150); |
|||
p.strokeWeight(1.5); |
|||
p.line(x, pad.top, x, p.height - pad.bottom); |
|||
|
|||
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(); |
|||
}; |
|||
}; |
|||
// Draw a circle on the CAN speed line at the current time.
|
|||
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(); // No stroke for the ellipse.
|
|||
p.ellipse(x, y, 8, 8); |
|||
} |
|||
} |
|||
/** |
|||
* Handles window resizing. Resizes the canvas and recreates/redraws the static buffer. |
|||
*/ |
|||
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(); |
|||
}; |
|||
}; |
|||
@ -1,19 +1,38 @@ |
|||
export const appState = { |
|||
|
|||
|
|||
vizData : null, |
|||
canData : [], |
|||
rawCanLogText : null, |
|||
videoStartDate : null, |
|||
radarStartTimeMs : 0, |
|||
isPlaying : false, |
|||
currentFrame : 0, |
|||
globalMinSnr : 0, globalMaxSnr : 1, |
|||
p5_instance : null, speedGraphInstance : null, |
|||
jsonFilename : '', videoFilename : '', canLogFilename : '', |
|||
isCloseUpMode : false, |
|||
masterClockStart : 0, |
|||
mediaTimeStart : 0, |
|||
lastSyncTime : 0, |
|||
|
|||
}; |
|||
// Stores the parsed visualization data (radar frames, tracks, etc.)
|
|||
vizData: null, |
|||
// Stores the processed CAN bus data (speed, time)
|
|||
canData: [], |
|||
// Temporarily holds raw CAN log text if video start date is not yet available for processing
|
|||
rawCanLogText: null, |
|||
// The Date object representing the start time of the video
|
|||
videoStartDate: null, |
|||
// The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename
|
|||
radarStartTimeMs: 0, |
|||
// Boolean indicating if the playback is currently active
|
|||
isPlaying: false, |
|||
// The index of the currently displayed radar frame
|
|||
currentFrame: 0, |
|||
// The global minimum SNR value across all radar frames, used for color scaling
|
|||
globalMinSnr: 0, |
|||
// The global maximum SNR value across all radar frames, used for color scaling
|
|||
globalMaxSnr: 1, |
|||
// Reference to the p5.js instance for the radar visualization
|
|||
p5_instance: null, |
|||
// Reference to the p5.js instance for the speed graph visualization
|
|||
speedGraphInstance: null, |
|||
// The filename of the loaded JSON file
|
|||
jsonFilename: "", |
|||
// The filename of the loaded video file
|
|||
videoFilename: "", |
|||
// The filename of the loaded CAN log file
|
|||
canLogFilename: "", |
|||
// Boolean indicating if the close-up interaction mode is active
|
|||
isCloseUpMode: false, |
|||
// Timestamp (from performance.now()) when the master clock started for synchronized playback
|
|||
masterClockStart: 0, |
|||
// The media time (in seconds) of the video when the master clock started
|
|||
mediaTimeStart: 0, |
|||
// Timestamp (from performance.now()) of the last synchronization check
|
|||
lastSyncTime: 0, |
|||
}; |
|||
@ -1,54 +1,58 @@ |
|||
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'); |
|||
const lightIcon = document.getElementById('theme-toggle-light-icon'); |
|||
import { appState } from "./state.js"; |
|||
import { videoPlayer } from "./dom.js"; |
|||
const themeToggleBtn = document.getElementById("theme-toggle"); |
|||
const darkIcon = document.getElementById("theme-toggle-dark-icon"); |
|||
const lightIcon = document.getElementById("theme-toggle-light-icon"); |
|||
|
|||
function setTheme(theme) { |
|||
if (theme === 'dark') { |
|||
document.documentElement.classList.add('dark'); |
|||
lightIcon.classList.remove('hidden'); |
|||
darkIcon.classList.add('hidden'); |
|||
localStorage.setItem('color-theme', 'dark'); |
|||
} else { |
|||
document.documentElement.classList.remove('dark'); |
|||
darkIcon.classList.remove('hidden'); |
|||
lightIcon.classList.add('hidden'); |
|||
localStorage.setItem('color-theme', 'light'); |
|||
} |
|||
|
|||
// Redraw the main radar plot
|
|||
if (appState.p5_instance) appState.p5_instance.redraw(); |
|||
if (theme === "dark") { |
|||
document.documentElement.classList.add("dark"); |
|||
lightIcon.classList.remove("hidden"); |
|||
darkIcon.classList.add("hidden"); |
|||
localStorage.setItem("color-theme", "dark"); |
|||
} else { |
|||
document.documentElement.classList.remove("dark"); |
|||
darkIcon.classList.remove("hidden"); |
|||
lightIcon.classList.add("hidden"); |
|||
localStorage.setItem("color-theme", "light"); |
|||
} |
|||
|
|||
// Redraw the main radar plot to apply theme changes
|
|||
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) { |
|||
// 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(); |
|||
// Redraw the speed graph to apply theme changes
|
|||
if (appState.speedGraphInstance) { |
|||
// Check if there's data available to draw on the speed graph
|
|||
if ( |
|||
(appState.canData.length > 0 || appState.vizData) && |
|||
videoPlayer.duration |
|||
) { |
|||
// If data exists, redraw the static parts of the graph to a buffer
|
|||
// This ensures the background and static elements reflect the new theme
|
|||
appState.speedGraphInstance.drawStaticGraphToBuffer( |
|||
appState.canData, |
|||
appState.vizData |
|||
); |
|||
} |
|||
// ================= END OF FIX =========================
|
|||
// Request a redraw of the speed graph to display the updated buffer
|
|||
appState.speedGraphInstance.redraw(); |
|||
} |
|||
} |
|||
|
|||
|
|||
export function initializeTheme() { |
|||
const savedTheme = localStorage.getItem('color-theme'); |
|||
if (savedTheme) { |
|||
setTheme(savedTheme); |
|||
const savedTheme = localStorage.getItem("color-theme"); |
|||
if (savedTheme) { |
|||
setTheme(savedTheme); |
|||
} else { |
|||
// Default to light mode if no theme is saved
|
|||
setTheme("light"); |
|||
} |
|||
|
|||
themeToggleBtn.addEventListener("click", () => { |
|||
if (document.documentElement.classList.contains("dark")) { |
|||
setTheme("light"); |
|||
} else { |
|||
// Default to light mode if no theme is saved
|
|||
setTheme('light'); |
|||
setTheme("dark"); |
|||
} |
|||
|
|||
themeToggleBtn.addEventListener('click', () => { |
|||
if (document.documentElement.classList.contains('dark')) { |
|||
setTheme('light'); |
|||
} else { |
|||
setTheme('dark'); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue