Visualizer work
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1389 lines
47 KiB

import {
RADAR_X_MAX,
RADAR_X_MIN,
RADAR_Y_MAX,
RADAR_Y_MIN,
MAX_TRAJECTORY_LENGTH,
ROI_TRACKS_Y_MIN,
ROI_TRACKS_Y_MAX,
ROI_CLOSE_Y_MIN,
ROI_CLOSE_Y_MAX,
} from "./constants.js";
import { appState } from "./state.js";
import {
toggleSnrColor,
toggleClusterColor,
toggleInlierColor,
toggleFrameNorm,
toggleVelocity,
toggleStationaryColor,
toggleConfirmedOnly,
togglePredictedPos,
toggleTracks,
} from "./dom.js";
// Defines a set of SNR (Signal-to-Noise Ratio) colors.
export const snrColors = (p) => ({
c1: p.color(0, 0, 255), // Blue
c2: p.color(0, 255, 255), // Cyan
c3: p.color(0, 255, 0), // Green
c4: p.color(186, 142, 35), // Dark Yellow
c5: p.color(255, 0, 0), // Red
});
// In src/drawUtils.js, add this near the other color constants
export const ttcColors = (p) => ({
critical: p.color(255, 0, 0), // Red for TTC <= 5s
high: p.color(255, 165, 0), // Orange for 5s < TTC <= 10s
medium: p.color(255, 255, 0), // Yellow for 10s < TTC <= 30s
low: p.color(0, 255, 0), // Green for TTC > 30s
away: p.color(0, 191, 255), // Deep Sky Blue for moving away
default: p.color(128, 128, 128), // Gray for unknown/default
});
// Defines a palette of 20 colors for different clusters.
export const clusterColors = (p) => [
// Primary & Secondary Colors
p.color(230, 25, 75), // 1. Red
p.color(60, 180, 75), // 2. Green
p.color(0, 130, 200), // 3. Blue
p.color(245, 130, 48), // 4. Orange
p.color(145, 30, 180), // 5. Purple
p.color(70, 240, 240), // 6. Cyan
// Tertiary & Bright Colors
p.color(240, 50, 230), // 7. Magenta
p.color(210, 245, 60), // 8. Lime
p.color(250, 190, 212), // 9. Pink
p.color(0, 128, 128), // 10. Teal
p.color(220, 190, 255), // 11. Lavender
p.color(170, 110, 40), // 12. Brown
p.color(255, 250, 200), // 13. Beige
p.color(128, 0, 0), // 14. Maroon
p.color(170, 255, 195), // 15. Mint
p.color(128, 128, 0), // 16. Olive
p.color(255, 215, 180), // 17. Apricot
p.color(0, 0, 128), // 18. Navy
p.color(70, 130, 180), // 19. Steel Blue (Replaced Gray as grey is for unclustered. )
p.color(255, 255, 25), // 20. Yellow
];
// Defines colors for stationary and moving objects.
export const stationaryColor = (p) => p.color(218, 165, 32); // Goldenrod
export const movingColor = (p) => p.color(255, 0, 255); // Magenta
/**
* Draws the static radar region lines to a buffer.
* @param {p5.Graphics} b - The p5.Graphics buffer to draw on.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawStaticRegionsToBuffer(p, b, plotScales) {
b.clear();
b.push();
// Translate to the bottom center of the buffer.
b.translate(b.width / 2, b.height * 0.95);
// Flip the Y-axis to match radar coordinates (Y increases upwards).
b.scale(1, -1);
// Set stroke properties for the static region lines.
b.stroke(100, 100, 100, 150);
b.strokeWeight(1);
// Set dashed line pattern.
b.drawingContext.setLineDash([8, 8]);
// Define angles for the radar beams.
const a1 = p.radians(30),
a2 = p.radians(150);
const len = 70;
// Draw the first static region line.
b.line(
0,
0,
len * p.cos(a1) * plotScales.plotScaleX,
len * p.sin(a1) * plotScales.plotScaleY
);
// Draw the second static region line.
b.line(
0,
0,
len * p.cos(a2) * plotScales.plotScaleX,
len * p.sin(a2) * plotScales.plotScaleY
);
// Reset line dash pattern.
b.drawingContext.setLineDash([]);
b.pop();
}
/**
* Draws the grid and axes for the radar plot.
* @param {p5} p - The p5 instance.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawAxes(p, plotScales) {
p.push();
// Determine axis and text colors based on the current theme (dark/light mode).
const axisColor = document.documentElement.classList.contains("dark")
? p.color(100)
: p.color(220);
const mainAxisColor = document.documentElement.classList.contains("dark")
? p.color(150)
: p.color(180);
const textColor = document.documentElement.classList.contains("dark")
? p.color(200)
: p.color(150);
// Draw horizontal grid lines.
p.stroke(axisColor);
p.strokeWeight(1);
for (let y = 5; y <= RADAR_Y_MAX; y += 5)
p.line(
RADAR_X_MIN * plotScales.plotScaleX,
y * plotScales.plotScaleY,
RADAR_X_MAX * plotScales.plotScaleX,
y * plotScales.plotScaleY
);
// Draw vertical grid lines.
const xGridStep = 5;
for (
let x = Math.ceil(RADAR_X_MIN / xGridStep) * xGridStep;
x <= RADAR_X_MAX;
x += xGridStep
) {
if (x === 0) continue;
p.line(
x * plotScales.plotScaleX,
RADAR_Y_MIN * plotScales.plotScaleY,
x * plotScales.plotScaleX,
RADAR_Y_MAX * plotScales.plotScaleY
);
}
p.stroke(mainAxisColor);
p.line(
RADAR_X_MIN * plotScales.plotScaleX,
0,
RADAR_X_MAX * plotScales.plotScaleX,
0
);
p.line(
0,
RADAR_Y_MIN * plotScales.plotScaleY,
0,
RADAR_Y_MAX * plotScales.plotScaleY
);
// Draw Y-axis labels.
p.fill(textColor);
p.noStroke();
p.textSize(10);
for (let y = 5; y <= RADAR_Y_MAX; y += 5) {
p.push();
p.translate(5, y * plotScales.plotScaleY);
// Flip text vertically to align with flipped Y-axis.
p.scale(1, -1);
p.text(y, 0, 4);
p.pop();
}
// Draw X-axis labels.
for (
let x = Math.ceil(RADAR_X_MIN / xGridStep) * xGridStep;
x <= RADAR_X_MAX;
x += xGridStep
) {
if (x === 0) continue;
p.push();
p.translate(x * plotScales.plotScaleX, -10);
p.scale(1, -1);
p.textAlign(p.CENTER);
p.text(x, 0, 0);
p.pop();
}
p.pop();
}
/**
* Draws the point cloud on the radar canvas.
* @param {p5} p - The p5 instance.
* @param {Array} points - The array of point cloud data.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawPointCloud(p, points, plotScales) {
// Set stroke weight for points.
p.strokeWeight(4);
// Get state of various toggles from the DOM.
const useSnr = toggleSnrColor.checked;
const useCluster = toggleClusterColor.checked;
const useInlier = toggleInlierColor.checked;
const useFrameNorm = toggleFrameNorm.checked;
let minSnr = appState.globalMinSnr, // Initialize with global SNR range.
maxSnr = appState.globalMaxSnr;
if (useSnr && useFrameNorm && points.length > 0) {
const snrVals = points.map((p) => p.snr).filter((snr) => snr !== null);
if (snrVals.length > 1) {
minSnr = Math.min(...snrVals);
maxSnr = Math.max(...snrVals);
} else if (snrVals.length === 1) {
minSnr = snrVals[0] - 1;
maxSnr = snrVals[0] + 1;
}
}
// Draw SNR legend if enabled and p5 instance is ready.
if (useSnr && p.drawSnrLegendToBuffer)
p.drawSnrLegendToBuffer(minSnr, maxSnr);
// Get local color instances for cluster and SNR.
const localClusterColors = clusterColors(p);
const localSnrColors = snrColors(p);
// Iterate through each point in the point cloud.
for (const pt of points) {
if (pt && pt.x !== null && pt.y !== null) {
// Apply cluster coloring if enabled.
if (useCluster && pt.clusterNumber !== null) {
p.stroke(
pt.clusterNumber > 0
? localClusterColors[
(pt.clusterNumber - 1) % localClusterColors.length
]
: 128
// Default to gray if cluster number is 0 or invalid.
);
} else if (useInlier) {
p.stroke(
pt.isOutlier === false
? p.color(0, 255, 0)
: pt.isOutlier === true
? p.color(255, 0, 0)
: 128
// Default to gray if inlier status is unknown.
);
} else if (useSnr && pt.snr !== null) {
const amt = p.map(pt.snr, minSnr, maxSnr, 0, 1, true);
let c;
if (amt < 0.25)
c = p.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
else if (amt < 0.5)
c = p.lerpColor(
localSnrColors.c2,
localSnrColors.c3,
(amt - 0.25) / 0.25
);
else if (amt < 0.75)
c = p.lerpColor(
localSnrColors.c3,
localSnrColors.c4,
(amt - 0.5) / 0.25
);
else
c = p.lerpColor(
localSnrColors.c4,
localSnrColors.c5,
(amt - 0.75) / 0.25
// Interpolate color based on SNR value.
);
p.stroke(c);
// Default point color if no specific coloring is applied.
} else {
p.stroke(0, 150, 255);
}
p.point(pt.x * plotScales.plotScaleX, pt.y * plotScales.plotScaleY);
}
}
}
/**
* Draws the historical trajectories of tracked objects.
* @param {p5} p - The p5 instance.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawTrajectories(p, plotScales) {
const localTtcColors = ttcColors(p);
for (const track of appState.vizData.tracks) {
if (toggleConfirmedOnly.checked && track.isConfirmed === false) {
continue;
}
if (!track || !track.historyLog || !Array.isArray(track.historyLog)) {
const trackId = track ? track.id : "Unknown ID";
console.warn(
`Skipping malformed track in frame ${appState.currentFrame}. Track ID: ${trackId}`,
track // We also log the entire track object for detailed inspection.
); // Safeguard for malformed data
continue;
}
const logs = track.historyLog.filter(
(log) => log.frameIdx <= appState.currentFrame + 1
);
if (logs.length < 2) continue;
const lastLog = logs[logs.length - 1];
if (appState.currentFrame + 1 - lastLog.frameIdx > MAX_TRAJECTORY_LENGTH)
continue;
const isCurrentlyStationary = lastLog.isStationary;
// ... (trajectory point calculation logic remains the same)
let maxLen = isCurrentlyStationary
? Math.floor(MAX_TRAJECTORY_LENGTH / 4)
: MAX_TRAJECTORY_LENGTH;
let trajPts = logs
.filter(
(log) => log.correctedPosition && log.correctedPosition[0] !== null
)
.map((log) => log.correctedPosition);
if (trajPts.length > maxLen) {
trajPts = trajPts.slice(trajPts.length - maxLen);
}
p.push();
p.noFill();
if (isCurrentlyStationary) {
// Stationary tracks are always green and dashed
p.stroke(34, 139, 34, 220);
p.strokeWeight(1);
p.drawingContext.setLineDash([3, 3]);
for (let i = 1; i < trajPts.length; i++) {
// ... (draw fading stationary trajectory logic)
}
} else {
// --- START: New Dynamic Coloring Logic ---
let trajectoryColor;
if (appState.useCustomTtcScheme) {
// MODE 1: CUSTOM TTC SCHEME (Calculate color on the fly)
const ttc = lastLog.ttc;
const scheme = appState.customTtcScheme;
if (ttc === null || isNaN(ttc) || ttc < 0) {
trajectoryColor = p.color(localTtcColors.default); // Gray for unknown
} else if (ttc <= scheme.critical.time) {
trajectoryColor = p.color(scheme.critical.color);
} else if (ttc <= scheme.high.time) {
trajectoryColor = p.color(scheme.high.color);
} else if (ttc <= scheme.medium.time) {
trajectoryColor = p.color(scheme.medium.color);
} else {
trajectoryColor = p.color(scheme.low.color); // Use custom color for low risk
}
} else {
// MODE 2: DEFAULT TTC SCHEME (Use pre-calculated category from JSON)
// FIND the TTC category from the new timeline
const ttcEntry = track.ttcCategoryTimeline.find(
(entry) => entry.frameIdx === lastLog.frameIdx
);
const ttcCategory = ttcEntry ? ttcEntry.ttcCategory : null; // Get the category if found
switch (ttcCategory) {
case 3:
trajectoryColor = p.color(localTtcColors.critical);
break;
case 2:
trajectoryColor = p.color(localTtcColors.high);
break;
case 1:
trajectoryColor = p.color(localTtcColors.medium);
break;
case 0:
trajectoryColor = p.color(localTtcColors.low);
break;
case -1:
trajectoryColor = p.color(localTtcColors.away);
break;
default:
trajectoryColor = p.color(localTtcColors.default);
break;
}
}
p.strokeWeight(1.5);
p.drawingContext.setLineDash([]);
// Fading trajectory logic (works for both modes)
for (let i = 1; i < trajPts.length; i++) {
const alpha = p.map(i, 0, trajPts.length, 50, 255);
trajectoryColor.setAlpha(alpha);
p.stroke(trajectoryColor);
const prevPt = trajPts[i - 1];
const currPt = trajPts[i];
p.line(
prevPt[0] * plotScales.plotScaleX,
prevPt[1] * plotScales.plotScaleY,
currPt[0] * plotScales.plotScaleX,
currPt[1] * plotScales.plotScaleY
);
}
// --- END: New Dynamic Coloring Logic ---
}
p.drawingContext.setLineDash([]);
p.pop();
}
}
/**
* Draws markers for the current position of tracked objects.
* @param {p5} p - The p5 instance.
* @param {object} plotScales - The calculated scales for plotting.
*/
// In src/drawUtils.js
export function drawTrackMarkers(p, plotScales) {
const showDetails = toggleVelocity.checked;
const useStationary = toggleStationaryColor.checked;
const textColor = document.documentElement.classList.contains("dark")
? p.color(255)
: p.color(0);
const localStationaryColor = stationaryColor(p);
const localMovingColor = movingColor(p);
for (const track of appState.vizData.tracks) {
// --- START: Add the Same Safeguard Here ---
// This robust check ensures the track and its historyLog are valid before use.
if (toggleConfirmedOnly.checked && track.isConfirmed === false) {
continue;
}
if (!track || !track.historyLog || !Array.isArray(track.historyLog)) {
// We don't need to log a warning here again, as drawTrajectories already did.
// We can just safely skip this malformed track.
continue;
}
// --- END: Add the Same Safeguard Here ---
const log = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame
);
if (log) {
const pos = log.correctedPosition;
if (pos && pos.length === 2 && pos[0] !== null && pos[1] !== null) {
const size = 5;
const x = pos[0] * plotScales.plotScaleX;
const y = pos[1] * plotScales.plotScaleY;
let velocityColor = p.color(255, 0, 255, 200);
p.push();
p.strokeWeight(2);
if (useStationary && log.isStationary === true) {
p.stroke(localStationaryColor);
p.noFill();
p.rectMode(p.CENTER);
p.square(x, y, size * 1.5);
velocityColor = localStationaryColor;
} else {
let markerColor = p.color(0, 0, 255);
if (useStationary && log.isStationary === false) {
markerColor = localMovingColor;
velocityColor = localMovingColor;
}
p.stroke(markerColor);
p.line(x - size, y, x + size, y);
p.line(x, y - size, x, y + size);
}
p.pop();
if (
showDetails &&
log.predictedVelocity &&
log.predictedVelocity[0] !== null
) {
const [vx, vy] = log.predictedVelocity;
if (log.isStationary === false) {
p.push();
p.stroke(velocityColor);
p.strokeWeight(2);
p.line(
x,
y,
(pos[0] + vx) * plotScales.plotScaleX,
(pos[1] + vy) * plotScales.plotScaleY
);
p.pop();
}
const speed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1);
const ttc =
log.ttc !== null && isFinite(log.ttc) && log.ttc < 100
? `TTC: ${log.ttc.toFixed(1)}s`
: "";
const text = `ID: ${track.id} | ${speed} km/h\n${ttc}`;
p.push();
p.fill(textColor);
p.noStroke();
p.scale(1, -1);
p.textSize(12);
p.text(text, x + 10, -y);
p.pop();
}
}
}
}
}
/**
* Handles the display of a comprehensive info tooltip for all elements under the mouse.
* @param {p5} p - The p5 instance.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
// --- Step 1: Gather Hovered Items ---
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (!frameData) return []; // Return empty array if no data
const hoveredItems = [];
// --- START: Dynamic Radius Logic ---
// The hover radius is now inversely proportional to the zoom factor.
const radius = p.constrain(80 / appState.zoomFactor, 5, 25);
// --- END: Dynamic Radius Logic ---
const localClusterColors = clusterColors(p); // <-- Get the color palette once
// ... (Step 1a: Find hovered points - no changes here) ...
if (frameData.pointCloud) {
// In steps/src/drawUtils.js
// Find hovered points
if (frameData.pointCloud) {
for (let i = 0; i < frameData.pointCloud.length; i++) {
const pt = frameData.pointCloud[i];
if (pt.x === null || pt.y === null) continue;
const screenX = pt.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY;
const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values
if (d < radius) {
// Add the index 'i' to the object we push
hoveredItems.push({
type: "point",
data: pt,
screenX,
screenY,
index: i,
});
}
}
}
}
// Find hovered cluster centroids
if (toggleClusterColor.checked && frameData.clusters) {
const clusters = Array.isArray(frameData.clusters)
? frameData.clusters
: [frameData.clusters];
for (const cluster of clusters) {
if (cluster.x === null || cluster.y === null) continue;
const screenX = cluster.x * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - cluster.y * plotScales.plotScaleY;
const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values
if (d < radius) {
const color =
cluster.id > 0
? localClusterColors[(cluster.id - 1) % localClusterColors.length]
: p.color(128);
hoveredItems.push({
type: "cluster",
data: cluster,
screenX,
screenY,
color: color,
});
}
}
}
// Find hovered track markers and predicted positions
if (appState.vizData.tracks) {
for (const track of appState.vizData.tracks) {
// --- FIX START: Fetch log for the CURRENT frame for the track marker ---
const currentLog = track.historyLog.find(
(log) => log.frameIdx === appState.currentFrame
);
// --- FIX END ---
if (currentLog) {
if (currentLog.correctedPosition && currentLog.correctedPosition[0] !== null) {
const pos = currentLog.correctedPosition;
const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pos[1] * plotScales.plotScaleY;
const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values
if (d < radius) {
hoveredItems.push({
type: "track",
data: currentLog, // Use the log for the current frame
trackId: track.id,
screenX,
screenY,
});
}
}
}
// For predicted position, we now also use the current frame's log.
if (currentLog) {
if (
togglePredictedPos.checked &&
currentLog.predictedPosition &&
currentLog.predictedPosition[0] !== null
) {
const pos = currentLog.predictedPosition;
const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
const screenY = p.height * 0.95 - pos[1] * plotScales.plotScaleY;
const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values
if (d < radius) {
hoveredItems.push({
type: "prediction",
data: currentLog,
trackId: track.id,
screenX,
screenY,
});
}
}
}
}
}
// Sort items by their vertical screen position to prevent crossed lines.
hoveredItems.sort((a, b) => a.screenY - b.screenY);
// If we aren't hovering over anything, draw nothing.
if (hoveredItems.length === 0) {
return hoveredItems; // Return the empty array
}
// --- Step 2 & 3: Generate Text and Render Tooltip ---
const infoStrings = [];
for (const item of hoveredItems) {
let infoText = "";
let itemColor = item.color || null; // Initialize with existing item color or null
const data = item.data;
switch (item.type) {
case "point":
const vel = data.velocity !== null ? data.velocity.toFixed(2) : "N/A";
const snr = data.snr !== null ? data.snr.toFixed(1) : "N/A";
infoText = `Point ${item.index} | X:${data.x.toFixed(
2
)}, Y:${data.y.toFixed(2)} | V:${vel}, SNR:${snr}, Cluster: ${
data.clusterNumber
}`;
break;
case "cluster":
const rs =
data.radialSpeed !== null ? data.radialSpeed.toFixed(2) : "N/A";
const vx = data.vx !== null ? data.vx.toFixed(2) : "N/A";
const vy = data.vy !== null ? data.vy.toFixed(2) : "N/A";
infoText = `Cluster ${data.id} | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(
2
)} | rSpeed:${rs}, vX:${vx}, vY:${vy}`;
// itemColor is already set for clusters when pushed to hoveredItems
break;
case "track":
const trackX = data.correctedPosition[0];
const trackY = data.correctedPosition[1];
let trackSpeed = "N/A";
if (
data.predictedVelocity &&
data.predictedVelocity[0] !== null &&
data.predictedVelocity[1] !== null
) {
const [vx, vy] = data.predictedVelocity;
// Calculate speed in km/h, similar to drawTrackMarkers
trackSpeed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1) + " km/h";
}
infoText = `Track ${item.trackId} | X:${trackX.toFixed(
2
)}, Y:${trackY.toFixed(2)} | Speed: ${trackSpeed}`;
// Check for dark mode to ensure visibility
const isDark = document.documentElement.classList.contains("dark");
itemColor = isDark
? p.color(100, 149, 237) // A lighter "Cornflower Blue" for dark mode
: p.color(0, 0, 255); // Original blue for light mode
break;
case "prediction":
const p_vx =
data.predictedVelocity[0] !== null
? data.predictedVelocity[0].toFixed(2)
: "N/A";
const p_vy =
data.predictedVelocity[1] !== null
? data.predictedVelocity[1].toFixed(2)
: "N/A";
infoText = `Pred. for ${item.trackId} | X:${data.predictedPosition[0].toFixed(
2
)}, Y:${data.predictedPosition[1].toFixed(2)} | vX:${p_vx}, vY:${p_vy}`;
itemColor = p.color(255, 0, 0); // Red color for prediction info
break;
}
if (infoText) {
infoStrings.push({ text: infoText, color: itemColor });
}
}
p.push();
p.textSize(12);
const lineHeight = 15;
const boxPadding = 8;
let boxWidth = 0;
for (const strInfo of infoStrings) {
boxWidth = Math.max(boxWidth, p.textWidth(strInfo.text));
}
const boxHeight = infoStrings.length * lineHeight + boxPadding * 2;
boxWidth += boxPadding * 2;
const xOffset = 20;
let boxX, lineAnchorX;
if (mouseX + xOffset + boxWidth > p.width) { // Use smoothed values
boxX = mouseX - boxWidth - xOffset;
lineAnchorX = boxX + boxWidth;
} else {
boxX = mouseX + xOffset;
lineAnchorX = boxX;
}
let boxY = p.mouseY - boxHeight / 2;
boxY = p.constrain(boxY, 0, p.height - boxHeight);
const highlightColor = p.color(46, 204, 113);
for (const item of hoveredItems) {
p.noFill();
p.stroke(highlightColor);
p.strokeWeight(2);
p.ellipse(item.screenX, item.screenY, 15, 15);
}
const bgColor = document.documentElement.classList.contains("dark")
? p.color(20, 20, 30, 220)
: p.color(245, 245, 245, 220);
p.fill(bgColor);
p.stroke(highlightColor);
p.strokeWeight(1);
p.rect(boxX, boxY, boxWidth, boxHeight, 4);
const defaultTextColor = document.documentElement.classList.contains("dark")
? p.color(230)
: p.color(20);
const dividerColor = document.documentElement.classList.contains("dark")
? p.color(80)
: p.color(200);
for (let i = 0; i < infoStrings.length; i++) {
const info = infoStrings[i];
const lineY = boxY + boxPadding + i * lineHeight;
if (i > 0) {
p.stroke(dividerColor);
p.strokeWeight(0.5);
p.line(boxX + 1, lineY, boxX + boxWidth - 1, lineY);
}
p.noStroke();
p.textAlign(p.LEFT, p.TOP);
p.fill(info.color || defaultTextColor);
p.text(info.text, boxX + boxPadding, lineY);
const item = hoveredItems[i];
const lineAnchorY = lineY + lineHeight / 2;
p.stroke(highlightColor);
p.strokeWeight(1);
p.line(lineAnchorX, lineAnchorY, item.screenX, item.screenY);
}
p.pop();
// Return the list of hovered items for other functions (like the zoom window) to use.
return hoveredItems;
}
export function drawCovarianceEllipse(
p,
position,
radii,
angle,
plotScales,
isStationary
) {
// Only draw the ellipse for tracks that are not stationary.
if (isStationary) return;
const [radiusA, radiusB] = radii;
const angledegrees = 90 + angle;
p.push();
p.noFill();
p.stroke(255, 0, 0, 150);
p.strokeWeight(1);
p.translate(
position[0] * plotScales.plotScaleX,
position[1] * plotScales.plotScaleY
);
p.rotate(p.radians(angledegrees));
p.ellipse(
0,
0,
radiusA * 2 * plotScales.plotScaleX, // multiplied by 2 because ellipse function
radiusB * 2 * plotScales.plotScaleY // in p5 library expect
);
p.pop();
//---old ellipse logic using covariance from data directly//
// const pPos = [
// [covarianceP[0][0], covarianceP[0][1]],
// [covarianceP[1][0], covarianceP[1][1]],
// ];
// const a = pPos[0][0];
// const b = pPos[0][1];
// const d = pPos[1][1];
// const trace = a + d;
// const determinant = a * d - b * b;
//const lambda1 = trace / 2 + Math.sqrt(Math.pow(trace, 2) / 4 - determinant);
//const lambda2 = trace / 2 - Math.sqrt(Math.pow(trace, 2) / 4 - determinant);
// --- START: New robust calculation with logging ---
// let sqrtTermVal = Math.pow(trace, 2) / 4 - determinant;
// Check for a negative value, which causes NaN errors
// if (sqrtTermVal < 0) {
// // Log a warning so we know it happened, as you suggested
// console.warn(
// `Clamping negative sqrtTermVal in frame ${appState.currentFrame} to prevent NaN. Original value: ${sqrtTermVal}`
// );
// // Clamp the value to 0. This allows drawing to continue instead of breaking.
// sqrtTermVal = 0;
// }
// const sqrtTerm = Math.sqrt(sqrtTermVal);
// const lambda1 = trace / 2 + sqrtTerm;
// const lambda2 = trace / 2 - sqrtTerm;
// // --- END: New robust calculation with logging ---
// const chi2 = 5.991;
// const majorAxis = Math.sqrt(chi2 * lambda1);
// const minorAxis = Math.sqrt(chi2 * lambda2);
// let eigenvector = [1, 0];
// if (b !== 0) {
// eigenvector = [lambda1 - d, b];
// }
// const angle = Math.atan2(eigenvector[1], eigenvector[0]);
//---old ellipse logic using covariance from data directly//
}
// In src/drawUtils.js
/**
* Draws a simple representation of the ego vehicle at the origin (0,0).
* @param {p5.Graphics} b - The p5.Graphics buffer to draw on.
*/
export function drawEgoVehicle(p, plotScales) {
const isDark = document.documentElement.classList.contains("dark");
const carColor = isDark ? p.color(150, 150, 220) : p.color(151, 151, 220);
p.push();
p.fill(carColor);
p.noStroke();
p.rectMode(p.CENTER);
const carWidthMeters = 1.5;
const carLengthMeters = 3.5;
const carWidthPixels = carWidthMeters * plotScales.plotScaleX;
const carLengthPixels = carLengthMeters * plotScales.plotScaleY;
p.rect(0, -10, carWidthPixels, carLengthPixels, 5);
p.pop();
}
//OLD_Solid Fill Logic
/**
* Draws the defined regions of interest (ROI) based on dynamic data from the current frame.
* @param {p5} p - The p5 instance to draw on.
* @param {object} frameData - The data for the current radar frame.
* @param {object} plotScales - The calculated scales for plotting.
*/
/**
*/
export function drawRegionsOfInterest(p, frameData, plotScales) {
// --- THIS CHECK IS ESSENTIAL AND MUST NOT BE REMOVED ---
// It gracefully handles frames that do not have the barrier data.
if (!frameData || !frameData.filtered_barrier_x) {
console.warn(
`Skipping bcoz no filtered barrier track in frame ${appState.currentFrame}. `,
frameData
);
return; // Exit the function if the data is missing for this frame.
}
//check here once
const isDark = document.documentElement.classList.contains("dark");
// Using brighter, more visible colors with transparency
const tracksRegionColor = isDark
? p.color(137, 207, 240, 50)
: p.color(173, 216, 230, 80);
const closeRegionColor = isDark
? p.color(255, 182, 193, 60)
: p.color(255, 182, 193, 90);
const [left, right] = frameData.filtered_barrier_x;
p.push();
p.stroke(1);
p.strokeWeight(1);
p.noFill();
p.rectMode(p.CORNERS); // console.warn(`Skipping bcoz no filtered barrier track in frame ${appState.currentFrame}. `, frameData);
// --- Draw Tracks Region ---
p.fill(tracksRegionColor);
p.rect(
left * plotScales.plotScaleX,
ROI_TRACKS_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_TRACKS_Y_MAX * plotScales.plotScaleY
);
// --- Draw Close Region ---
p.fill(closeRegionColor);
p.rect(
left * plotScales.plotScaleX,
ROI_CLOSE_Y_MIN * plotScales.plotScaleY,
right * plotScales.plotScaleX,
ROI_CLOSE_Y_MAX * plotScales.plotScaleY
);
p.pop();
}
//OLD_Solid Fill Logic
/**
* Draws the cluster centroids on the radar canvas as an asterisk.
* Handles cases where a single cluster is an object instead of an array.
* @param {p5} p - The p5 instance.
* @param {Array|object} clustersInput - The cluster data for the current frame.
* @param {object} plotScales - The calculated scales for plotting.
*/
export function drawClusterCentroids(p, clustersInput, plotScales) {
if (!clustersInput) {
return; // Do nothing if there's no cluster data
}
// --- START: Robustness Fix ---
// This check handles the data inconsistency. If clustersInput is not an array,
// we wrap the single cluster object in an array so the loop works consistently.
const clusters = Array.isArray(clustersInput)
? clustersInput
: [clustersInput];
// --- END: Robustness Fix ---
if (clusters.length === 0) {
return; // Exit if the resulting array is empty
}
const localClusterColors = clusterColors(p);
for (const cluster of clusters) {
if (
cluster &&
typeof cluster.x === "number" &&
typeof cluster.y === "number"
) {
const x = cluster.x * plotScales.plotScaleX;
const y = cluster.y * plotScales.plotScaleY;
const color =
cluster.id > 0
? localClusterColors[(cluster.id - 1) % localClusterColors.length]
: p.color(128);
p.push();
p.stroke(color);
p.strokeWeight(1.5);
const armLength = 5;
p.line(x, y - armLength, x, y + armLength);
p.line(x - armLength, y, x + armLength, y);
p.line(
x - armLength * 0.7,
y - armLength * 0.7,
x + armLength * 0.7,
y + armLength * 0.7
);
p.line(
x + armLength * 0.7,
y - armLength * 0.7,
x - armLength * 0.7,
y + armLength * 0.7
);
p.pop();
}
}
}
//--- drawClusterCentroids function---//
// old trial functions to replace the close up display.
// In src/drawUtils.js
// Replace the ENTIRE 'handleCloseUpDisplay' function with these TWO new functions:
// /**
// * Finds all radar elements (points, tracks, etc.) under the mouse cursor.
// * @param {p5} p - The p5 instance (for mouse coordinates and distance checks).
// * @param {object} plotScales - The calculated scales for plotting.
// * @returns {Array} An array of hovered item objects.
// */
// export function findHoveredItems(p, plotScales) {
// const frameData = appState.vizData.radarFrames[appState.currentFrame];
// if (!frameData) return [];
// const hoveredItems = [];
// const radius = 10;
// const localClusterColors = clusterColors(p);
// // Find hovered points
// if (frameData.pointCloud) {
// for (const pt of frameData.pointCloud) {
// if (pt.x === null || pt.y === null) continue;
// const screenX = pt.x * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (pt.y * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// hoveredItems.push({ type: 'point', data: pt, screenX, screenY });
// }
// }
// }
// // Find hovered cluster centroids
// if (toggleClusterColor.checked && frameData.clusters) {
// const clusters = Array.isArray(frameData.clusters) ? frameData.clusters : [frameData.clusters];
// for (const cluster of clusters) {
// if (cluster.x === null || cluster.y === null) continue;
// const screenX = cluster.x * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (cluster.y * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// const color = cluster.id > 0 ? localClusterColors[(cluster.id - 1) % localClusterColors.length] : p.color(128);
// hoveredItems.push({ type: 'cluster', data: cluster, screenX, screenY, color });
// }
// }
// }
// // Find hovered tracks and predictions
// if (appState.vizData.tracks) {
// for (const track of appState.vizData.tracks) {
// const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
// if (log) {
// if (log.correctedPosition && log.correctedPosition[0] !== null) {
// const pos = log.correctedPosition;
// const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (pos[1] * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// hoveredItems.push({ type: 'track', data: log, trackId: track.id, screenX, screenY });
// }
// }
// if (togglePredictedPos.checked && log.predictedPosition && log.predictedPosition[0] !== null) {
// const pos = log.predictedPosition;
// const screenX = pos[0] * plotScales.plotScaleX + p.width / 2;
// const screenY = p.height * 0.95 - (pos[1] * plotScales.plotScaleY);
// if (p.dist(p.mouseX, p.mouseY, screenX, screenY) < radius) {
// hoveredItems.push({ type: 'prediction', data: log, trackId: track.id, screenX, screenY });
// }
// }
// }
// }
// }
// hoveredItems.sort((a, b) => a.screenY - b.screenY);
// return hoveredItems;
// }
// /**
// * Draws the visual tooltip and connectors for a given list of hovered items.
// * @param {p5} p - The p5 instance to draw with.
// * @param {Array} hoveredItems - An array of items from findHoveredItems.
// */
// export function drawTooltip(p, hoveredItems) {
// if (hoveredItems.length === 0) return;
// const infoStrings = [];
// // Generate display text
// for (const item of hoveredItems) {
// let infoText = '';
// const data = item.data;
// switch (item.type) {
// case 'point':
// infoText = `Point | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(2)} | V:${data.velocity?.toFixed(2)}, SNR:${data.snr?.toFixed(1)}`;
// break;
// case 'cluster':
// infoText = `Cluster ${data.id} | X:${data.x.toFixed(2)}, Y:${data.y.toFixed(2)} | rSpeed:${data.radialSpeed?.toFixed(2)}`;
// break;
// case 'track':
// infoText = `Track ${item.trackId} | X:${data.correctedPosition[0].toFixed(2)}, Y:${data.correctedPosition[1].toFixed(2)}`;
// break;
// case 'prediction':
// infoText = `Pred. for ${item.trackId} | X:${data.predictedPosition[0].toFixed(2)}, Y:${data.predictedPosition[1].toFixed(2)}`;
// break;
// }
// if (infoText) {
// infoStrings.push({ text: infoText, color: item.color || null });
// }
// }
// p.push();
// p.textSize(12);
// const lineHeight = 15;
// const boxPadding = 8;
// let boxWidth = 0;
// infoStrings.forEach(info => {
// boxWidth = Math.max(boxWidth, p.textWidth(info.text));
// });
// const boxHeight = (infoStrings.length * lineHeight) + (boxPadding * 2);
// boxWidth += (boxPadding * 2);
// const xOffset = 20;
// let boxX = p.mouseX + xOffset;
// if (boxX + boxWidth > p.width) {
// boxX = p.mouseX - boxWidth - xOffset;
// }
// let boxY = p.mouseY - (boxHeight / 2);
// boxY = p.constrain(boxY, 0, p.height - boxHeight);
// // Draw highlights and connectors
// const highlightColor = p.color(46, 204, 113);
// hoveredItems.forEach((item, i) => {
// p.noFill();
// p.stroke(highlightColor);
// p.strokeWeight(2);
// p.ellipse(item.screenX, item.screenY, 15, 15);
// p.strokeWeight(1);
// const lineAnchorX = boxX < p.mouseX ? boxX + boxWidth : boxX;
// p.line(lineAnchorX, boxY + boxPadding + (i * lineHeight) + (lineHeight / 2), item.screenX, item.screenY);
// });
// // Draw the box and text
// const bgColor = document.documentElement.classList.contains('dark') ? p.color(20, 20, 30, 220) : p.color(245, 245, 245, 220);
// p.fill(bgColor);
// p.stroke(highlightColor);
// p.strokeWeight(1);
// p.rect(boxX, boxY, boxWidth, boxHeight, 4);
// const defaultTextColor = document.documentElement.classList.contains('dark') ? p.color(230) : p.color(20);
// p.noStroke();
// p.textAlign(p.LEFT, p.TOP);
// infoStrings.forEach((info, i) => {
// p.fill(info.color || defaultTextColor);
// p.text(info.text, boxX + boxPadding, boxY + boxPadding + (i * lineHeight));
// });
// p.pop();
// }
// // /**
// // * Renders a high-fidelity, zoomed-in view of the scene around the mouse cursor.
// // * @param {p5} p - The p5 instance.
// // * @param {object} plotScales - The calculated scales for plotting.
// // * @param {Array} hoveredItems - The array of items currently under the mouse.
// // */
// // export function drawZoomWindow(p, plotScales, hoveredItems) {
// // // --- Zoom Window Configuration (easily modifiable) ---
// // // The magnification level. 4.0 means 4x zoom.
// // const zoomFactor = 4.0;
// // // The output size of the zoom window on the screen, in pixels.
// // const zoomWindowWidth = 250;
// // const zoomWindowHeight = 250;
// // // Position the zoom window in the bottom-right of the canvas.
// // const boxX = p.width - zoomWindowWidth - 20;
// // const boxY = p.height - zoomWindowHeight - 20;
// // p.push(); // Save the current global drawing state.
// // // --- Create a "Portal" to the Zoomed View ---
// // // We use a clipping mask to ensure the zoomed content doesn't spill out.
// // p.drawingContext.save();
// // p.drawingContext.rect(boxX, boxY, zoomWindowWidth, zoomWindowHeight);
// // p.drawingContext.clip();
// // // We now transform the entire canvas coordinate system for the redraw.
// // p.translate(boxX, boxY); // 1. Move origin to the zoom box's corner.
// // p.scale(zoomFactor); // 2. Scale everything up.
// // // 3. Translate so the mouse position is in the center of the box.
// // p.translate(-p.mouseX + zoomWindowWidth / (2 * zoomFactor), -p.mouseY + zoomWindowHeight / (2 * zoomFactor));
// // // --- Redraw the Entire Scene in the New Zoomed Coordinate System ---
// // // This provides a high-fidelity, not just pixelated, zoom.
// // p.background(document.documentElement.classList.contains('dark') ? p.color(55, 65, 81) : 255);
// // p.image(p.get(), 0, 0); // A trick to redraw the static background buffer
// // p.push(); // Nested push for the main radar transformations.
// // p.translate(p.width / 2, p.height * 0.95);
// // p.scale(1, -1);
// // const frameData = appState.vizData.radarFrames[appState.currentFrame];
// // drawAxes(p, plotScales);
// // drawEgoVehicle(p, plotScales);
// // if (frameData) {
// // drawRegionsOfInterest(p, frameData, plotScales);
// // if (toggleTracks.checked) {
// // drawTrajectories(p, plotScales);
// // drawTrackMarkers(p, plotScales);
// // }
// // drawPointCloud(p, frameData.pointCloud, plotScales);
// // if (toggleClusterColor.checked) {
// // drawClusterCentroids(p, frameData.clusters, plotScales);
// // }
// // // Redraw predicted positions if toggled
// // if (togglePredictedPos.checked) {
// // for (const track of appState.vizData.tracks) {
// // const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame + 1);
// // if (log && log.predictedPosition && log.predictedPosition[0] !== null) {
// // const pos = log.predictedPosition;
// // const x = pos[0] * plotScales.plotScaleX;
// // const y = pos[1] * plotScales.plotScaleY;
// // p.push();
// // p.stroke(255, 0, 0); p.strokeWeight(2);
// // p.line(x - 4, y - 4, x + 4, y + 4);
// // p.line(x + 4, y - 4, x - 4, y + 4);
// // p.pop();
// // }
// // }
// // }
// // }
// // p.pop(); // End of radar transformations.
// // // --- Redraw Tooltip and Connectors ---
// // // We re-run the original tooltip function, which will now draw inside our zoomed view,
// // // making the connector lines perfectly accurate.
// // handleCloseUpDisplay(p, plotScales);
// // // Clean up the clipping mask.
// // p.drawingContext.restore();
// // // --- Draw Border and Crosshairs on Top of Everything ---
// // p.noFill();
// // p.stroke(46, 204, 113); // Highlight green border.
// // p.strokeWeight(2);
// // p.rect(boxX, boxY, zoomWindowWidth, zoomWindowHeight);
// // // Red crosshairs to mark the exact mouse position.
// // const crosshairSize = 10;
// // p.stroke(255, 0, 0, 150);
// // p.strokeWeight(1);
// // p.line(boxX + zoomWindowWidth/2 - crosshairSize, boxY + zoomWindowHeight/2, boxX + zoomWindowWidth/2 + crosshairSize, boxY + zoomWindowHeight/2);
// // p.line(boxX + zoomWindowWidth/2, boxY + zoomWindowHeight/2 - crosshairSize, boxX + zoomWindowWidth/2, boxY + zoomWindowHeight/2 + crosshairSize);
// // p.pop(); // Restore the original global drawing state.
// // }
// OLD HATCH FILL logic
// /**
// * Draws a hatched pattern inside a rectangle defined by corner points.
// * This is a new helper function.
// * @param {p5.Graphics} b The p5.Graphics buffer to draw on.
// * @param {number} x1 The x-coordinate of the first corner.
// * @param {number} y1 The y-coordinate of the first corner.
// * @param {number} x2 The x-coordinate of the second corner.
// * @param {number} y2 The y-coordinate of the second corner.
// * @param {p5.Color} hatchColor The color of the hatches.
// * @param {number} spacing The distance between hatch lines.
// */
// function drawHatchedRect(b, x1, y1, x2, y2, hatchColor, spacing) {
// const minX = Math.min(x1, x2);
// const maxX = Math.max(x1, x2);
// const minY = Math.min(y1, y2);
// const maxY = Math.max(y1, y2);
// b.push();
// b.stroke(hatchColor);
// b.strokeWeight(2);
// b.noFill();
// // To draw lines like '/', we use the equation y = x + c (positive slope).
// // The constant 'c' is equal to y - x.
// // We iterate 'c' through its possible range for the given rectangle.
// // The minimum value for c is minY - maxX, and the maximum is maxY - minX.
// for (let c = minY - maxX; c < maxY - minX; c += spacing) {
// let points = [];
// // For a line y = x + c:
// // Intersection with top edge (y = maxY) -> x = maxY - c
// let ix_top = maxY - c;
// if (ix_top >= minX && ix_top <= maxX) points.push({ x: ix_top, y: maxY });
// // Intersection with bottom edge (y = minY) -> x = minY - c
// let ix_bottom = minY - c;
// if (ix_bottom >= minX && ix_bottom <= maxX) {
// points.push({ x: ix_bottom, y: minY });
// }
// // Intersection with left edge (x = minX) -> y = minX + c
// let iy_left = minX + c;
// if (iy_left >= minY && iy_left <= maxY) {
// points.push({ x: minX, y: iy_left });
// }
// // Intersection with right edge (x = maxX) -> y = maxX + c
// let iy_right = maxX + c;
// if (iy_right >= minY && iy_right <= maxY) {
// points.push({ x: maxX, y: iy_right });
// }
// // A line can only intersect a convex shape (like a rectangle) at two points.
// // If it passes through a corner, we might get duplicates.
// if (points.length >= 2) {
// // Remove duplicate points
// const uniquePoints = [];
// const seen = new Set();
// for (const p of points) {
// const key = `${p.x},${p.y}`;
// if (!seen.has(key)) {
// uniquePoints.push(p);
// seen.add(key);
// }
// }
// if (uniquePoints.length >= 2) {
// b.line(
// uniquePoints[0].x,
// uniquePoints[0].y,
// uniquePoints[1].x,
// uniquePoints[1].y
// );
// }
// }
// }
// b.pop();
// }
// /**
// * Draws the defined regions of interest (ROI) onto the canvas buffer.
// * @param {p5} p - The p5 instance.
// * @param {p5.Graphics} b - The p5.Graphics buffer to draw on.
// * @param {object} plotScales - The calculated scales for plotting.
// */
// export function drawRegionsOfInterest(p, b, plotScales) {
// const isDark = document.documentElement.classList.contains("dark");
// // Define semi-transparent colors for the hatching. Opacity is higher for lines.
// const tracksRegionColor = isDark
// ? p.color(137, 207, 240, 50)
// : p.color(173, 216, 230, 100); // Light blue
// const closeRegionColor = isDark
// ? p.color(255, 182, 193, 60)
// : p.color(255, 182, 193, 120); // Light pink
// b.push();
// b.translate(b.width / 2, b.height * 0.95); // Translate to the bottom center of the buffer, same as other static elements
// b.scale(1, -1); // Flip Y-axis
// // --- Draw Tracks Region ---
// drawHatchedRect(
// b,
// ROI_TRACKS_X_MIN * plotScales.plotScaleX,
// ROI_TRACKS_Y_MIN * plotScales.plotScaleY,
// ROI_TRACKS_X_MAX * plotScales.plotScaleX,
// ROI_TRACKS_Y_MAX * plotScales.plotScaleY,
// tracksRegionColor,
// 15 // Spacing for the hatch lines
// );
// // --- Draw Close Region ---
// drawHatchedRect(
// b,
// ROI_CLOSE_X_MIN * plotScales.plotScaleX,
// ROI_CLOSE_Y_MIN * plotScales.plotScaleY,
// ROI_CLOSE_X_MAX * plotScales.plotScaleX,
// ROI_CLOSE_Y_MAX * plotScales.plotScaleY,
// closeRegionColor,
// 15 // Spacing for the hatch lines
// );
// b.pop();
// }