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
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();
|
|
// }
|