|
|
@ -1,9 +1,11 @@ |
|
|
|
|
|
// File: src/speedGraphSketch.js
|
|
|
import { appState } from "../state.js"; |
|
|
import { appState } from "../state.js"; |
|
|
import { videoPlayer, speedGraphContainer } from "../dom.js"; |
|
|
import { videoPlayer, speedGraphContainer } from "../dom.js"; |
|
|
|
|
|
|
|
|
export const speedGraphSketch = function (p) { |
|
|
export const speedGraphSketch = function (p) { |
|
|
let staticBuffer, minSpeed, maxSpeed, videoDuration; |
|
|
let staticBuffer, minSpeed, maxSpeed, videoDuration; |
|
|
const pad = { top: 20, right: 130, bottom: 30, left: 50 }; |
|
|
|
|
|
|
|
|
// Reserve more top space for legend and reduce the right padding so the plot can use more width.
|
|
|
|
|
|
const pad = { top: 48, right: 20, bottom: 30, left: 50 }; |
|
|
|
|
|
|
|
|
p.drawStaticGraphToBuffer = function (radarData) { |
|
|
p.drawStaticGraphToBuffer = function (radarData) { |
|
|
const b = staticBuffer; |
|
|
const b = staticBuffer; |
|
|
@ -16,17 +18,16 @@ export const speedGraphSketch = function (p) { |
|
|
b.push(); |
|
|
b.push(); |
|
|
b.stroke(gridColor); |
|
|
b.stroke(gridColor); |
|
|
b.strokeWeight(1); |
|
|
b.strokeWeight(1); |
|
|
|
|
|
// Y axis
|
|
|
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( |
|
|
|
|
|
pad.left, |
|
|
|
|
|
b.height - pad.bottom, |
|
|
|
|
|
b.width - pad.right, |
|
|
|
|
|
b.height - pad.bottom |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// X axis across the new plotting width
|
|
|
|
|
|
b.line(pad.left, b.height - pad.bottom, b.width - pad.right, b.height - pad.bottom); |
|
|
|
|
|
|
|
|
b.textAlign(b.RIGHT, b.CENTER); |
|
|
b.textAlign(b.RIGHT, b.CENTER); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
b.fill(textColor); |
|
|
b.fill(textColor); |
|
|
b.textSize(10); |
|
|
b.textSize(10); |
|
|
|
|
|
|
|
|
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); |
|
|
@ -40,51 +41,41 @@ 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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
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); |
|
|
|
|
|
|
|
|
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); |
|
|
b.text(Math.round(t), x, b.height - pad.bottom + 5); |
|
|
b.text(Math.round(t), x, b.height - pad.bottom + 5); |
|
|
} |
|
|
} |
|
|
b.fill(textColor); |
|
|
b.fill(textColor); |
|
|
b.text("Time (s)", b.width / 2, b.height - pad.bottom + 18); |
|
|
|
|
|
|
|
|
b.text("Time (s)", (pad.left + (b.width - pad.right)) / 2, b.height - pad.bottom + 18); |
|
|
b.pop(); |
|
|
b.pop(); |
|
|
|
|
|
|
|
|
|
|
|
// Draw CAN speed (solid blue)
|
|
|
if (radarData && radarData.radarFrames) { |
|
|
if (radarData && radarData.radarFrames) { |
|
|
b.noFill(); |
|
|
b.noFill(); |
|
|
b.stroke(0, 150, 255); // Blue for CAN speed
|
|
|
|
|
|
|
|
|
b.stroke(0, 150, 255); |
|
|
b.strokeWeight(1.5); |
|
|
b.strokeWeight(1.5); |
|
|
b.beginShape(); |
|
|
b.beginShape(); |
|
|
for (const frame of radarData.radarFrames) { |
|
|
for (const frame of radarData.radarFrames) { |
|
|
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (frame.canVehSpeed_kmph === null || isNaN(frame.canVehSpeed_kmph)) continue; |
|
|
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 y = b.map( |
|
|
|
|
|
frame.canVehSpeed_kmph, |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Draw Ego speed (dashed green)
|
|
|
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]); |
|
|
@ -92,21 +83,9 @@ 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); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -114,44 +93,72 @@ export const speedGraphSketch = function (p) { |
|
|
b.drawingContext.setLineDash([]); |
|
|
b.drawingContext.setLineDash([]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// --- Legend: centered in the top padding area (above the plotting area) ---
|
|
|
b.push(); |
|
|
b.push(); |
|
|
b.strokeWeight(2); |
|
|
|
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
b.fill(textColor); |
|
|
b.fill(textColor); |
|
|
|
|
|
b.textSize(12); |
|
|
b.textAlign(b.LEFT, b.CENTER); |
|
|
b.textAlign(b.LEFT, b.CENTER); |
|
|
|
|
|
|
|
|
|
|
|
const canLabel = "CAN Speed"; |
|
|
|
|
|
const egoLabel = "Ego Speed"; |
|
|
|
|
|
|
|
|
|
|
|
const segLen = 18; |
|
|
|
|
|
const gapBetweenSegAndLabel = 8; |
|
|
|
|
|
const betweenItemsGap = 24; |
|
|
|
|
|
|
|
|
|
|
|
// compute widths of each legend item (segment + gap + label)
|
|
|
|
|
|
const canItemWidth = segLen + gapBetweenSegAndLabel + b.textWidth(canLabel); |
|
|
|
|
|
const egoItemWidth = segLen + gapBetweenSegAndLabel + b.textWidth(egoLabel); |
|
|
|
|
|
const totalLegendWidth = canItemWidth + betweenItemsGap + egoItemWidth; |
|
|
|
|
|
|
|
|
|
|
|
// center the legend across the plotting region (pad.left .. b.width - pad.right)
|
|
|
|
|
|
const plottingLeft = pad.left; |
|
|
|
|
|
const plottingRight = b.width - pad.right; |
|
|
|
|
|
const centerX = (plottingLeft + plottingRight) / 2; |
|
|
|
|
|
const legendStartX = centerX - totalLegendWidth / 2; |
|
|
|
|
|
const legendY = pad.top / 2; // vertically centered inside the top padding
|
|
|
|
|
|
|
|
|
|
|
|
// Draw CAN legend item
|
|
|
|
|
|
b.push(); |
|
|
b.stroke(0, 150, 255); |
|
|
b.stroke(0, 150, 255); |
|
|
b.line(b.width - 120, pad.top + 10, b.width - 100, pad.top + 10); |
|
|
|
|
|
|
|
|
b.strokeWeight(2); |
|
|
|
|
|
b.line(legendStartX, legendY + 6, legendStartX + segLen, legendY + 6); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
b.text("CAN Speed", b.width - 95, pad.top + 10); |
|
|
|
|
|
|
|
|
b.fill(textColor); |
|
|
|
|
|
b.text(canLabel, legendStartX + segLen + gapBetweenSegAndLabel, legendY + 6); |
|
|
|
|
|
b.pop(); |
|
|
|
|
|
|
|
|
|
|
|
// Draw Ego legend item
|
|
|
|
|
|
const egoX = legendStartX + canItemWidth + betweenItemsGap; |
|
|
|
|
|
b.push(); |
|
|
b.stroke(0, 200, 100); |
|
|
b.stroke(0, 200, 100); |
|
|
|
|
|
b.strokeWeight(2); |
|
|
b.drawingContext.setLineDash([3, 3]); |
|
|
b.drawingContext.setLineDash([3, 3]); |
|
|
b.line(b.width - 120, pad.top + 30, b.width - 100, pad.top + 30); |
|
|
|
|
|
|
|
|
b.line(egoX, legendY + 6, egoX + segLen, legendY + 6); |
|
|
b.drawingContext.setLineDash([]); |
|
|
b.drawingContext.setLineDash([]); |
|
|
b.noStroke(); |
|
|
b.noStroke(); |
|
|
b.text("Ego Speed", b.width - 95, pad.top + 30); |
|
|
|
|
|
|
|
|
b.fill(textColor); |
|
|
|
|
|
b.text(egoLabel, egoX + segLen + gapBetweenSegAndLabel, legendY + 6); |
|
|
|
|
|
b.pop(); |
|
|
|
|
|
|
|
|
b.pop(); |
|
|
b.pop(); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
p.setup = function () { |
|
|
p.setup = function () { |
|
|
let canvas = p.createCanvas( |
|
|
|
|
|
speedGraphContainer.offsetWidth, |
|
|
|
|
|
speedGraphContainer.offsetHeight |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
let canvas = p.createCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight); |
|
|
canvas.parent("speed-graph-container"); |
|
|
canvas.parent("speed-graph-container"); |
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
p.noLoop(); |
|
|
p.noLoop(); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
p.setData = function (radarData, duration) { |
|
|
p.setData = function (radarData, duration) { |
|
|
|
|
|
|
|
|
if (!radarData || !radarData.radarFrames) return; |
|
|
if (!radarData || !radarData.radarFrames) return; |
|
|
videoDuration = duration; // Accept duration, even if it's 0 or NaN initially
|
|
|
|
|
|
|
|
|
videoDuration = duration; |
|
|
|
|
|
|
|
|
let speeds = []; |
|
|
let speeds = []; |
|
|
if (radarData && radarData.radarFrames) { |
|
|
if (radarData && radarData.radarFrames) { |
|
|
const egoSpeeds = radarData.radarFrames.map( |
|
|
|
|
|
(frame) => frame.egoVelocity[1] * 3.6 |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const egoSpeeds = radarData.radarFrames.map((frame) => frame.egoVelocity[1] * 3.6); |
|
|
speeds.push(...egoSpeeds); |
|
|
speeds.push(...egoSpeeds); |
|
|
|
|
|
|
|
|
const canSpeeds = radarData.radarFrames |
|
|
const canSpeeds = radarData.radarFrames |
|
|
@ -160,24 +167,17 @@ export const speedGraphSketch = function (p) { |
|
|
speeds.push(...canSpeeds); |
|
|
speeds.push(...canSpeeds); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
minSpeed = |
|
|
|
|
|
speeds.length > 0 ? Math.floor(Math.min(...speeds) / 10) * 10 : 0; |
|
|
|
|
|
maxSpeed = |
|
|
|
|
|
speeds.length > 0 ? Math.ceil(Math.max(...speeds) / 10) * 10 : 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; |
|
|
if (maxSpeed <= 0) maxSpeed = 10; |
|
|
if (maxSpeed <= 0) maxSpeed = 10; |
|
|
if (minSpeed >= 0) minSpeed = 0; |
|
|
if (minSpeed >= 0) minSpeed = 0; |
|
|
|
|
|
|
|
|
// *** KEY CHANGE ***
|
|
|
|
|
|
// Only try to draw the static graph if the duration is valid.
|
|
|
|
|
|
if (videoDuration > 0) { |
|
|
if (videoDuration > 0) { |
|
|
p.drawStaticGraphToBuffer(radarData); |
|
|
p.drawStaticGraphToBuffer(radarData); |
|
|
} |
|
|
} |
|
|
//p.redraw();
|
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
p.draw = function () { |
|
|
p.draw = function () { |
|
|
// *** KEY CHANGE ***
|
|
|
|
|
|
// If duration is not ready, show a waiting message and stop
|
|
|
|
|
|
if (!videoDuration || videoDuration <= 0) { |
|
|
if (!videoDuration || videoDuration <= 0) { |
|
|
const isDark = document.documentElement.classList.contains("dark"); |
|
|
const isDark = document.documentElement.classList.contains("dark"); |
|
|
p.background(isDark ? [55, 65, 81] : 255); |
|
|
p.background(isDark ? [55, 65, 81] : 255); |
|
|
@ -191,51 +191,36 @@ export const speedGraphSketch = function (p) { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
function drawTimeIndicator() { |
|
|
function drawTimeIndicator() { |
|
|
// This new, more robust check is the fix. It ensures that the video duration is valid AND
|
|
|
|
|
|
// the main application has initialized the currentFrame before attempting to draw.
|
|
|
|
|
|
if ( |
|
|
if ( |
|
|
!videoDuration || |
|
|
!videoDuration || |
|
|
videoDuration <= 0 || |
|
|
videoDuration <= 0 || |
|
|
appState.currentFrame === null || |
|
|
appState.currentFrame === null || |
|
|
appState.currentFrame === undefined |
|
|
appState.currentFrame === undefined |
|
|
) { |
|
|
) { |
|
|
return; // Stop here if the state is not ready
|
|
|
|
|
|
|
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Get the current frame's data as the single source of truth
|
|
|
|
|
|
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
|
|
const frameData = appState.vizData.radarFrames[appState.currentFrame]; |
|
|
if (!frameData) return; // Exit if data for the specific frame isn't ready
|
|
|
|
|
|
|
|
|
if (!frameData) return; |
|
|
|
|
|
|
|
|
// Calculate the X position from the current frame's precise timestamp
|
|
|
|
|
|
const currentTimeSec = frameData.timestampMs / 1000.0; |
|
|
const currentTimeSec = frameData.timestampMs / 1000.0; |
|
|
const x = p.map( |
|
|
|
|
|
currentTimeSec, |
|
|
|
|
|
0, |
|
|
|
|
|
videoDuration, |
|
|
|
|
|
pad.left, |
|
|
|
|
|
p.width - pad.right |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Draw the red time indicator line at the accurate X position
|
|
|
|
|
|
|
|
|
const x = p.map(currentTimeSec, 0, videoDuration, pad.left, p.width - pad.right); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
// Now, draw the circle using the same frame data
|
|
|
|
|
|
if (frameData.canVehSpeed_kmph !== null && !isNaN(frameData.canVehSpeed_kmph)) { |
|
|
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.noStroke(); |
|
|
|
|
|
p.ellipse(x, y, 8, 8); |
|
|
|
|
|
|
|
|
const canSpeed = frameData.canVehSpeed_kmph; |
|
|
|
|
|
const y = p.map(canSpeed, 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.windowResized = function () { |
|
|
p.resizeCanvas( |
|
|
|
|
|
speedGraphContainer.offsetWidth, |
|
|
|
|
|
speedGraphContainer.offsetHeight |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
p.resizeCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight); |
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
staticBuffer = p.createGraphics(p.width, p.height); |
|
|
if (appState.vizData && videoDuration > 0) { |
|
|
if (appState.vizData && videoDuration > 0) { |
|
|
p.drawStaticGraphToBuffer(appState.vizData); |
|
|
p.drawStaticGraphToBuffer(appState.vizData); |
|
|
|