|
|
@ -1,37 +1,20 @@ |
|
|
//---Import APPSTATE VIDEOPLAYER and FindLastCanIndex---//
|
|
|
|
|
|
|
|
|
|
|
|
import { appState } from "../state.js"; |
|
|
import { appState } from "../state.js"; |
|
|
import { videoPlayer, speedGraphContainer } from "../dom.js"; |
|
|
import { videoPlayer, speedGraphContainer } from "../dom.js"; |
|
|
import { findLastCanIndexBefore } from "../utils.js"; |
|
|
|
|
|
|
|
|
|
|
|
export const speedGraphSketch = function (p) { |
|
|
export const speedGraphSketch = function (p) { |
|
|
// Declare variables for the static buffer, min/max speed for scaling, and video duration.
|
|
|
|
|
|
let staticBuffer, minSpeed, maxSpeed, videoDuration; |
|
|
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 }; |
|
|
const pad = { top: 20, right: 130, bottom: 30, left: 50 }; |
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 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) { |
|
|
|
|
|
|
|
|
p.drawStaticGraphToBuffer = function (radarData) { |
|
|
const b = staticBuffer; |
|
|
const b = staticBuffer; |
|
|
b.clear(); |
|
|
b.clear(); |
|
|
const isDark = document.documentElement.classList.contains("dark"); |
|
|
const isDark = document.documentElement.classList.contains("dark"); |
|
|
b.background(isDark ? [55, 65, 81] : 255); |
|
|
b.background(isDark ? [55, 65, 81] : 255); |
|
|
const gridColor = isDark ? 100 : 200; |
|
|
const gridColor = isDark ? 100 : 200; |
|
|
const textColor = isDark ? 200 : 100; // Determine text color based on theme.
|
|
|
|
|
|
|
|
|
const textColor = isDark ? 200 : 100; |
|
|
|
|
|
|
|
|
// Push current drawing style settings onto a stack.
|
|
|
|
|
|
b.push(); |
|
|
b.push(); |
|
|
// Set stroke for grid lines.
|
|
|
|
|
|
b.stroke(gridColor); |
|
|
b.stroke(gridColor); |
|
|
// Set stroke weight for grid lines.
|
|
|
|
|
|
b.strokeWeight(1); |
|
|
b.strokeWeight(1); |
|
|
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom); |
|
|
b.line(pad.left, pad.top, pad.left, b.height - pad.bottom); |
|
|
b.line( |
|
|
b.line( |
|
|
@ -39,14 +22,11 @@ export const speedGraphSketch = function (p) { |
|
|
b.height - pad.bottom, |
|
|
b.height - pad.bottom, |
|
|
b.width - pad.right, |
|
|
b.width - pad.right, |
|
|
b.height - pad.bottom |
|
|
b.height - pad.bottom |
|
|
); // Draw Y and X axes.
|
|
|
|
|
|
// Set text alignment for Y-axis labels.
|
|
|
|
|
|
|
|
|
); |
|
|
b.textAlign(b.RIGHT, b.CENTER); |
|
|
b.textAlign(b.RIGHT, b.CENTER); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
b.fill(textColor); |
|
|
b.fill(textColor); |
|
|
// Set text size for labels.
|
|
|
|
|
|
b.textSize(10); |
|
|
b.textSize(10); |
|
|
// Draw horizontal grid lines and speed labels.
|
|
|
|
|
|
for (let s = minSpeed; s <= maxSpeed; s += 10) { |
|
|
for (let s = minSpeed; s <= maxSpeed; s += 10) { |
|
|
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|
|
const y = b.map(s, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|
|
b.text(s, pad.left - 8, y); |
|
|
b.text(s, pad.left - 8, y); |
|
|
@ -60,14 +40,11 @@ export const speedGraphSketch = function (p) { |
|
|
b.line(pad.left + 1, y, b.width - pad.right, y); |
|
|
b.line(pad.left + 1, y, b.width - pad.right, y); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
} |
|
|
} |
|
|
// Draw Y-axis unit label.
|
|
|
|
|
|
b.fill(textColor); |
|
|
b.fill(textColor); |
|
|
b.text("km/h", pad.left - 8, pad.top - 8); |
|
|
b.text("km/h", pad.left - 8, pad.top - 8); |
|
|
// Set text alignment for X-axis labels.
|
|
|
|
|
|
b.textAlign(b.CENTER, b.TOP); |
|
|
b.textAlign(b.CENTER, b.TOP); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
b.fill(isDark ? 180 : 150); |
|
|
b.fill(isDark ? 180 : 150); |
|
|
// Calculate time interval for X-axis labels.
|
|
|
|
|
|
const tInt = Math.max(1, Math.floor(videoDuration / 10)); |
|
|
const tInt = Math.max(1, Math.floor(videoDuration / 10)); |
|
|
for (let t = 0; t <= videoDuration; t += tInt) { |
|
|
for (let t = 0; t <= videoDuration; t += tInt) { |
|
|
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right); |
|
|
const x = b.map(t, 0, videoDuration, pad.left, b.width - pad.right); |
|
|
@ -75,39 +52,27 @@ export const speedGraphSketch = function (p) { |
|
|
} |
|
|
} |
|
|
b.fill(textColor); |
|
|
b.fill(textColor); |
|
|
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18); |
|
|
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18); |
|
|
// Restore previous drawing style settings.
|
|
|
|
|
|
b.pop(); |
|
|
b.pop(); |
|
|
|
|
|
|
|
|
// Draw CAN speed data line if available.
|
|
|
|
|
|
if (canSpeedData && canSpeedData.length > 0) { |
|
|
|
|
|
b.noFill(); // Do not fill the shape.
|
|
|
|
|
|
b.stroke(0, 150, 255); |
|
|
|
|
|
|
|
|
if (radarData && radarData.radarFrames) { |
|
|
|
|
|
b.noFill(); |
|
|
|
|
|
b.stroke(0, 150, 255); // Blue for CAN speed
|
|
|
b.strokeWeight(1.5); |
|
|
b.strokeWeight(1.5); |
|
|
b.beginShape(); |
|
|
b.beginShape(); |
|
|
for (const d of canSpeedData) { |
|
|
|
|
|
const relTime = (d.time - appState.videoStartDate.getTime()) / 1000; |
|
|
|
|
|
|
|
|
for (const frame of radarData.radarFrames) { |
|
|
|
|
|
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
const relTime = frame.timestampMs / 1000; |
|
|
if (relTime >= 0 && relTime <= videoDuration) { |
|
|
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 |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); |
|
|
|
|
|
const y = b.map(frame.canVehSpeed_kmph, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|
|
b.vertex(x, y); |
|
|
b.vertex(x, y); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
b.endShape(); |
|
|
b.endShape(); |
|
|
} // End of CAN speed data drawing.
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Draw radar ego speed data line if available.
|
|
|
|
|
|
if (radarData && radarData.radarFrames) { |
|
|
if (radarData && radarData.radarFrames) { |
|
|
b.stroke(0, 200, 100); |
|
|
b.stroke(0, 200, 100); |
|
|
b.drawingContext.setLineDash([5, 5]); |
|
|
b.drawingContext.setLineDash([5, 5]); |
|
|
@ -115,29 +80,16 @@ export const speedGraphSketch = function (p) { |
|
|
for (const frame of radarData.radarFrames) { |
|
|
for (const frame of radarData.radarFrames) { |
|
|
const relTime = frame.timestampMs / 1000; |
|
|
const relTime = frame.timestampMs / 1000; |
|
|
if (relTime >= 0 && relTime <= videoDuration) { |
|
|
if (relTime >= 0 && relTime <= videoDuration) { |
|
|
const x = b.map( |
|
|
|
|
|
relTime, |
|
|
|
|
|
0, |
|
|
|
|
|
videoDuration, |
|
|
|
|
|
pad.left, |
|
|
|
|
|
b.width - pad.right |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const x = b.map(relTime, 0, videoDuration, pad.left, b.width - pad.right); |
|
|
const egoSpeedKmh = frame.egoVelocity[1] * 3.6; |
|
|
const egoSpeedKmh = frame.egoVelocity[1] * 3.6; |
|
|
const y = b.map( |
|
|
|
|
|
egoSpeedKmh, |
|
|
|
|
|
minSpeed, |
|
|
|
|
|
maxSpeed, |
|
|
|
|
|
b.height - pad.bottom, |
|
|
|
|
|
pad.top |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const y = b.map(egoSpeedKmh, minSpeed, maxSpeed, b.height - pad.bottom, pad.top); |
|
|
b.vertex(x, y); |
|
|
b.vertex(x, y); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
b.endShape(); |
|
|
b.endShape(); |
|
|
b.drawingContext.setLineDash([]); // Reset line dash to solid.
|
|
|
|
|
|
} // End of radar ego speed data drawing.
|
|
|
|
|
|
|
|
|
b.drawingContext.setLineDash([]); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Draw legend for the graph lines.
|
|
|
|
|
|
b.push(); |
|
|
b.push(); |
|
|
b.strokeWeight(2); |
|
|
b.strokeWeight(2); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
@ -155,110 +107,87 @@ export const speedGraphSketch = function (p) { |
|
|
b.text("Ego Speed", b.width - 95, pad.top + 30); |
|
|
b.text("Ego Speed", b.width - 95, pad.top + 30); |
|
|
b.pop(); |
|
|
b.pop(); |
|
|
}; |
|
|
}; |
|
|
/** |
|
|
|
|
|
* p5.js setup function. Initializes the canvas and static buffer. |
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
p.setup = function () { |
|
|
p.setup = function () { |
|
|
let canvas = p.createCanvas( |
|
|
let canvas = p.createCanvas( |
|
|
speedGraphContainer.offsetWidth, |
|
|
speedGraphContainer.offsetWidth, |
|
|
speedGraphContainer.offsetHeight |
|
|
speedGraphContainer.offsetHeight |
|
|
); |
|
|
); |
|
|
canvas.parent("speed-graph-container"); |
|
|
canvas.parent("speed-graph-container"); |
|
|
// Create an off-screen graphics buffer for static elements.
|
|
|
|
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
// Disable continuous looping; draw will be called manually.
|
|
|
|
|
|
p.noLoop(); |
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p.setData = function (radarData, duration) { |
|
|
|
|
|
if (!radarData || !radarData.radarFrames) return; |
|
|
videoDuration = duration; |
|
|
videoDuration = duration; |
|
|
|
|
|
|
|
|
let speeds = []; |
|
|
let speeds = []; |
|
|
if (canSpeedData) { |
|
|
|
|
|
speeds.push(...canSpeedData.map((d) => parseFloat(d.speed))); |
|
|
|
|
|
} |
|
|
|
|
|
if (radarData && radarData.radarFrames) { |
|
|
if (radarData && radarData.radarFrames) { |
|
|
const egoSpeeds = radarData.radarFrames.map( |
|
|
const egoSpeeds = radarData.radarFrames.map( |
|
|
(frame) => frame.egoVelocity[1] * 3.6 |
|
|
(frame) => frame.egoVelocity[1] * 3.6 |
|
|
); |
|
|
); |
|
|
speeds.push(...egoSpeeds); |
|
|
speeds.push(...egoSpeeds); |
|
|
|
|
|
|
|
|
|
|
|
const canSpeeds = radarData.radarFrames |
|
|
|
|
|
.map((frame) => frame.canVehSpeed_kmph) |
|
|
|
|
|
.filter((speed) => speed !== null && !isNaN(speed)); |
|
|
|
|
|
speeds.push(...canSpeeds); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
|
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 (maxSpeed <= 0) maxSpeed = 10; |
|
|
// Ensure minSpeed is 0 if all speeds are non-negative.
|
|
|
|
|
|
if (minSpeed >= 0) minSpeed = 0; |
|
|
if (minSpeed >= 0) minSpeed = 0; |
|
|
|
|
|
|
|
|
// Redraw the static graph elements to the buffer with new data.
|
|
|
|
|
|
p.drawStaticGraphToBuffer(canSpeedData, radarData); |
|
|
|
|
|
// Request a redraw of the main canvas.
|
|
|
|
|
|
|
|
|
p.drawStaticGraphToBuffer(radarData); |
|
|
p.redraw(); |
|
|
p.redraw(); |
|
|
}; |
|
|
}; |
|
|
/** |
|
|
|
|
|
* p5.js draw function. Draws the static buffer and the dynamic time indicator. |
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
p.draw = function () { |
|
|
p.draw = function () { |
|
|
if (!videoDuration) return; // Only draw if video duration is set.
|
|
|
|
|
|
|
|
|
if (!videoDuration) return; |
|
|
p.image(staticBuffer, 0, 0); |
|
|
p.image(staticBuffer, 0, 0); |
|
|
drawTimeIndicator(); |
|
|
drawTimeIndicator(); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
function drawTimeIndicator() { |
|
|
function drawTimeIndicator() { |
|
|
const currentTime = videoPlayer.currentTime; |
|
|
|
|
|
|
|
|
// Get the current frame's data as the single source of truth
|
|
|
|
|
|
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
|
|
|
|
|
if (!frameData) return; // Exit if data isn't ready
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the X position from the current frame's precise timestamp
|
|
|
|
|
|
const currentTimeSec = frameData.timestampMs / 1000.0; |
|
|
const x = p.map( |
|
|
const x = p.map( |
|
|
currentTime, |
|
|
|
|
|
|
|
|
currentTimeSec, |
|
|
0, |
|
|
0, |
|
|
videoDuration, |
|
|
videoDuration, |
|
|
pad.left, |
|
|
pad.left, |
|
|
p.width - pad.right |
|
|
p.width - pad.right |
|
|
); // Map current time to X-coordinate.
|
|
|
|
|
|
// Draw the red time indicator line.
|
|
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Draw the red time indicator line at the accurate X position
|
|
|
p.stroke(255, 0, 0, 150); |
|
|
p.stroke(255, 0, 0, 150); |
|
|
p.strokeWeight(1.5); |
|
|
p.strokeWeight(1.5); |
|
|
p.line(x, pad.top, x, p.height - pad.bottom); |
|
|
p.line(x, pad.top, x, p.height - pad.bottom); |
|
|
|
|
|
|
|
|
// 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 |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// Now, draw the circle using the same frame data
|
|
|
|
|
|
if (frameData.canVehSpeed_kmph !== null && !isNaN(frameData.canVehSpeed_kmph)) { |
|
|
|
|
|
const canSpeed = frameData.canVehSpeed_kmph; |
|
|
|
|
|
const y = p.map(canSpeed, minSpeed, maxSpeed, p.height - pad.bottom, pad.top); |
|
|
p.fill(255, 0, 0); |
|
|
p.fill(255, 0, 0); |
|
|
p.noStroke(); // No stroke for the ellipse.
|
|
|
|
|
|
|
|
|
p.noStroke(); |
|
|
p.ellipse(x, y, 8, 8); |
|
|
p.ellipse(x, y, 8, 8); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
/** |
|
|
|
|
|
* Handles window resizing. Resizes the canvas and recreates/redraws the static buffer. |
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
p.windowResized = function () { |
|
|
p.windowResized = function () { |
|
|
p.resizeCanvas( |
|
|
p.resizeCanvas( |
|
|
speedGraphContainer.offsetWidth, |
|
|
speedGraphContainer.offsetWidth, |
|
|
speedGraphContainer.offsetHeight |
|
|
speedGraphContainer.offsetHeight |
|
|
); |
|
|
); |
|
|
// Instead of resizing the buffer, we re-create it
|
|
|
|
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
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); |
|
|
|
|
|
|
|
|
if (appState.vizData && videoDuration) { |
|
|
|
|
|
p.drawStaticGraphToBuffer(appState.vizData); |
|
|
} |
|
|
} |
|
|
p.redraw(); |
|
|
p.redraw(); |
|
|
}; |
|
|
}; |