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.
540 lines
19 KiB
540 lines
19 KiB
import { appState } from "../state.js";
|
|
import { debugFlags } from "../debug.js";
|
|
import {
|
|
RADAR_X_MAX,
|
|
// Define radar plot boundaries
|
|
RADAR_X_MIN,
|
|
RADAR_Y_MAX,
|
|
RADAR_Y_MIN,
|
|
} from "../constants.js";
|
|
import {
|
|
canvasContainer,
|
|
toggleSnrColor,
|
|
toggleTracks,
|
|
togglePredictedPos,
|
|
toggleCovariance,
|
|
toggleVelocity,
|
|
toggleClusterColor,
|
|
} from "../dom.js";
|
|
import {
|
|
drawStaticRegionsToBuffer,
|
|
drawAxes,
|
|
drawPointCloud,
|
|
drawTrajectories,
|
|
drawEgoVehicle,
|
|
drawTrackMarkers,
|
|
snrColors,
|
|
handleCloseUpDisplay,
|
|
drawCovarianceEllipse,
|
|
ttcColors,
|
|
drawRegionsOfInterest,
|
|
drawClusterCentroids,
|
|
} from "../drawUtils.js";
|
|
|
|
export const radarSketch = function (p) {
|
|
// Object to store calculated plot scales
|
|
let plotScales = {
|
|
plotScaleX: 1,
|
|
plotScaleY: 1,
|
|
};
|
|
// p5.Graphics buffers for static elements to optimize drawing
|
|
let staticBackgroundBuffer, snrLegendBuffer, trackLegendBuffer;
|
|
|
|
// --- START: Mouse Smoothing Variables ---
|
|
let smoothedMouseX = 0;
|
|
let smoothedMouseY = 0;
|
|
let isFirstFrame = true; // Flag to initialize smoothed position
|
|
// --- END: Mouse Smoothing Variables ---
|
|
|
|
// --- START: FPS Calculation Variables ---
|
|
let lastFrameTime = 0;
|
|
// --- END: FPS Calculation Variables ---
|
|
|
|
// Helper function to allow other sketches to access the static background
|
|
p.getStaticBackground = function () {
|
|
return staticBackgroundBuffer;
|
|
};
|
|
// Function to calculate scaling factors for radar coordinates to canvas pixels
|
|
function calculatePlotScales() {
|
|
// Padding and offset values for the plot area
|
|
const hPad = 0.05,
|
|
vPad = 0.05,
|
|
bOff = 0.05;
|
|
// Calculate available width and height for the plot
|
|
const aW = p.width * (1 - 2 * hPad);
|
|
const aH = p.height * (1 - bOff - vPad);
|
|
// Determine plot scales based on radar boundaries and available canvas space
|
|
plotScales.plotScaleX = aW / (RADAR_X_MAX - RADAR_X_MIN);
|
|
plotScales.plotScaleY = aH / (RADAR_Y_MAX - RADAR_Y_MIN);
|
|
}
|
|
|
|
p.setup = function () {
|
|
// Create the p5.js canvas and attach it to the specified DOM element
|
|
let canvas = p.createCanvas(
|
|
canvasContainer.offsetWidth,
|
|
canvasContainer.offsetHeight
|
|
);
|
|
canvas.parent("canvas-container");
|
|
// --- START: ADD MOUSE WHEEL LISTENER HERE ---
|
|
canvas.mouseWheel((event) => {
|
|
// Only run this logic if the close-up mode is active
|
|
if (appState.isCloseUpMode) {
|
|
event.preventDefault(); // Prevent the page from scrolling
|
|
|
|
const zoomSpeed = 0.5;
|
|
const direction = Math.sign(event.deltaY);
|
|
let newZoomFactor = appState.zoomFactor - direction * zoomSpeed;
|
|
|
|
// Clamp the zoom factor to a reasonable range
|
|
newZoomFactor = p.constrain(newZoomFactor, 1.5, 30);
|
|
appState.zoomFactor = newZoomFactor;
|
|
|
|
// IMPORTANT: We must manually trigger a redraw of the zoom sketch
|
|
// so it immediately updates with the new zoom factor.
|
|
if (
|
|
appState.zoomSketchInstance &&
|
|
appState.zoomSketchInstance.updateAndDraw
|
|
) {
|
|
// We just need to trigger an update; the zoom sketch will read the new
|
|
// appState.zoomFactor when it redraws.
|
|
// We find the current hovered items again to pass them.
|
|
const hoveredItems = handleCloseUpDisplay(p, plotScales);
|
|
appState.zoomSketchInstance.updateAndDraw(
|
|
p.mouseX,
|
|
p.mouseY,
|
|
hoveredItems,
|
|
plotScales
|
|
);
|
|
}
|
|
}
|
|
});
|
|
// --- END: ADD MOUSE WHEEL LISTENER HERE ---
|
|
|
|
// Initialize graphics buffers
|
|
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
|
|
snrLegendBuffer = p.createGraphics(100, 450);
|
|
trackLegendBuffer = p.createGraphics(120, 120); // create track legend
|
|
|
|
calculatePlotScales();
|
|
p.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr);
|
|
p.drawTrackLegendToBuffer(); // Call the new function to draw the legend
|
|
|
|
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
|
|
p.noLoop();
|
|
// Disable continuous looping, redraw will be called manually
|
|
};
|
|
|
|
p.draw = function () {
|
|
if (debugFlags.drawing) console.log("draw_DEBUG: radarSketch.draw() called.");
|
|
|
|
// --- START: FPS Calculation & Display ---
|
|
const currentTime = p.millis();
|
|
if (lastFrameTime > 0) {
|
|
const delta = currentTime - lastFrameTime;
|
|
if (delta > 0) {
|
|
const currentFps = 1000 / delta;
|
|
// Use exponential moving average for smoothing
|
|
const smoothingFactor = 0.95;
|
|
appState.fps =
|
|
appState.fps * smoothingFactor + currentFps * (1 - smoothingFactor);
|
|
}
|
|
}
|
|
lastFrameTime = currentTime;
|
|
// --- END: FPS Calculation & Display ---
|
|
|
|
// Set background color based on current theme (dark/light)
|
|
p.background(
|
|
document.documentElement.classList.contains("dark")
|
|
? p.color(55, 65, 81)
|
|
: 255
|
|
);
|
|
// If no visualization data is loaded, stop drawing
|
|
if (!appState.vizData) return;
|
|
|
|
// Draw the pre-rendered static background elements
|
|
p.image(staticBackgroundBuffer, 0, 0);
|
|
|
|
// Apply transformations for radar coordinate system (origin at bottom-center, Y-axis inverted)
|
|
p.push();
|
|
p.translate(p.width / 2, p.height * 0.95);
|
|
p.scale(1, -1);
|
|
|
|
// Recalculate plot scales (important for window resizing)
|
|
calculatePlotScales();
|
|
// Draw coordinate axes
|
|
drawAxes(p, plotScales);
|
|
drawEgoVehicle(p, plotScales);
|
|
// Get current frame data
|
|
const frameData = appState.vizData.radarFrames[appState.currentFrame];
|
|
if (frameData) {
|
|
drawPointCloud(p, frameData.pointCloud, plotScales);
|
|
if (!appState.isRawOnlyMode) {
|
|
drawRegionsOfInterest(p, frameData, plotScales);
|
|
drawTrackMarkers(p, plotScales);
|
|
|
|
// Draw object trajectories and markers if enabled
|
|
// if (toggleVelocity.checked) {
|
|
// drawTrackMarkers(p, plotScales);
|
|
// }
|
|
if (togglePredictedPos.checked) {
|
|
for (const track of appState.vizData.tracks) {
|
|
const log = track.historyLog.find(
|
|
(log) => log.frameIdx === appState.currentFrame
|
|
);
|
|
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); // Red for predicted
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (toggleTracks.checked) {
|
|
drawTrajectories(p, plotScales);
|
|
if (toggleCovariance.checked) {
|
|
for (const track of appState.vizData.tracks) {
|
|
const log = track.historyLog.find(
|
|
(log) => log.frameIdx === appState.currentFrame + 1
|
|
);
|
|
if (
|
|
log &&
|
|
log.ellipseRadii &&
|
|
typeof log.ellipseAngle !== "undefined"
|
|
) {
|
|
const pos = log.predictedPosition;
|
|
if (pos && pos[0] !== null) {
|
|
drawCovarianceEllipse(
|
|
p,
|
|
pos,
|
|
log.ellipseRadii,
|
|
log.ellipseAngle,
|
|
plotScales,
|
|
log.isStationary
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw cluster centroids if enabled
|
|
if (toggleClusterColor.checked) {
|
|
drawClusterCentroids(p, frameData.clusters, plotScales);
|
|
}
|
|
}
|
|
}
|
|
p.pop();
|
|
|
|
// 4. Draw the new legend buffer onto the main canvas
|
|
// This is placed at the bottom-right corner.
|
|
if (toggleTracks.checked && !appState.isRawOnlyMode) {
|
|
p.image(
|
|
trackLegendBuffer,
|
|
p.width - trackLegendBuffer.width - 10,
|
|
p.height - trackLegendBuffer.height - 20
|
|
);
|
|
}
|
|
// End main radar transformations
|
|
|
|
// BUG FIX 1: Call the close-up handler if the mode is active
|
|
// --- Zoom and Tooltip Logic ---
|
|
const COOLING_PERIOD_MS = 2000; // Set to 3 seconds for the countdown
|
|
const zoomPanel = document.getElementById("zoom-panel");
|
|
if (appState.isCloseUpMode) {
|
|
// --- START: Mouse Smoothing Logic ---
|
|
// On the first frame of zoom, snap the smoothed position to the real mouse position.
|
|
if (isFirstFrame) {
|
|
smoothedMouseX = p.mouseX;
|
|
smoothedMouseY = p.mouseY;
|
|
isFirstFrame = false;
|
|
}
|
|
|
|
// The smoothing factor. A smaller value (e.g., 0.1) means more smoothing.
|
|
// This can be adjusted to feel more or less responsive.
|
|
const smoothingFactor = 0.5;
|
|
|
|
// Linearly interpolate the smoothed position towards the actual mouse position.
|
|
smoothedMouseX = p.lerp(smoothedMouseX, p.mouseX, smoothingFactor);
|
|
smoothedMouseY = p.lerp(smoothedMouseY, p.mouseY, smoothingFactor);
|
|
|
|
// Use the smoothed coordinates for all subsequent zoom-related calculations.
|
|
const hoveredItems = handleCloseUpDisplay(p, plotScales, smoothedMouseX, smoothedMouseY);
|
|
// --- END: Mouse Smoothing Logic ---
|
|
|
|
// --- START: Draw Zoom Area Rectangle & Debug Circle ---
|
|
const zoomWindow = document.getElementById("zoom-canvas-container");
|
|
if (zoomWindow) {
|
|
const zoomWindowWidth = zoomWindow.offsetWidth;
|
|
const zoomWindowHeight = zoomWindow.offsetHeight;
|
|
|
|
// Calculate the dimensions of the source rectangle on the main canvas.
|
|
const sourceWidth = zoomWindowWidth / appState.zoomFactor;
|
|
const sourceHeight = zoomWindowHeight / appState.zoomFactor;
|
|
|
|
// --- START: THEME-AWARE COLOR LOGIC ---
|
|
const isDark = document.documentElement.classList.contains("dark");
|
|
const rectColor = isDark ? p.color(255, 80, 80, 200) : p.color(220, 20, 60, 180);
|
|
// --- END: THEME-AWARE COLOR LOGIC ---
|
|
|
|
p.push();
|
|
p.noFill();
|
|
p.stroke(rectColor);
|
|
p.strokeWeight(1); // Reduced thickness.
|
|
p.drawingContext.setLineDash([5, 3]); // Dashed line.
|
|
p.rectMode(p.CENTER);
|
|
p.rect(smoothedMouseX, smoothedMouseY, sourceWidth, sourceHeight); // Use smoothed values
|
|
p.drawingContext.setLineDash([]); // Reset line dash
|
|
p.pop();
|
|
}
|
|
|
|
// Draw a temporary debug circle representing the hover radius.
|
|
// This formula must match the one in drawUtils.js for accurate visualization.
|
|
const hoverRadius = p.constrain(80 / appState.zoomFactor, 5, 25);
|
|
const isDark = document.documentElement.classList.contains("dark");
|
|
const circleColor = isDark ? p.color(50, 205, 50, 200) : p.color(0, 128, 0, 180);
|
|
|
|
p.push();
|
|
p.noFill();
|
|
p.stroke(circleColor);
|
|
p.strokeWeight(1);
|
|
p.ellipse(smoothedMouseX, smoothedMouseY, hoverRadius * 2, hoverRadius * 2); // Use smoothed values
|
|
p.pop();
|
|
// --- END: Draw Zoom Area Rectangle & Debug Circle ---
|
|
|
|
if (hoveredItems.length > 0) {
|
|
// If we are hovering, cancel any existing countdown.
|
|
clearTimeout(appState.zoomHideDelayTimeout);
|
|
appState.zoomHideDelayTimeout = null;
|
|
clearInterval(appState.zoomCountdownInterval);
|
|
appState.zoomCountdownInterval = null;
|
|
appState.zoomCountdown = null;
|
|
|
|
if (zoomPanel.style.display !== "block") {
|
|
zoomPanel.style.display = "block";
|
|
}
|
|
if (
|
|
appState.zoomSketchInstance &&
|
|
appState.zoomSketchInstance.updateAndDraw
|
|
) {
|
|
appState.zoomSketchInstance.updateAndDraw(
|
|
smoothedMouseX, // Use smoothed values
|
|
smoothedMouseY, // Use smoothed values
|
|
hoveredItems,
|
|
plotScales
|
|
);
|
|
}
|
|
} else if (zoomPanel.style.display === "block") {
|
|
// --- START: FIX for Grace Period Freeze ---
|
|
// If NOT hovering, but the panel is still visible, we must continue
|
|
// to update the zoom sketch so it follows the mouse.
|
|
// We pass an empty array for hoveredItems, so no tooltip is drawn.
|
|
if (
|
|
appState.zoomSketchInstance &&
|
|
appState.zoomSketchInstance.updateAndDraw
|
|
) {
|
|
appState.zoomSketchInstance.updateAndDraw(
|
|
smoothedMouseX, // Use smoothed values
|
|
smoothedMouseY, // Use smoothed values
|
|
[], // Pass empty array to hide tooltips
|
|
plotScales
|
|
);
|
|
}
|
|
// --- END: FIX for Grace Period Freeze ---
|
|
// 2. If a "hide" timer isn't already running, start one.
|
|
if (!appState.zoomHideDelayTimeout && !appState.zoomCountdownInterval) {
|
|
// Start a 2-second delay before the countdown begins.
|
|
appState.zoomHideDelayTimeout = setTimeout(() => {
|
|
appState.zoomHideDelayTimeout = null; // Clear the delay timer ID
|
|
// Now, start the actual 3-second countdown interval.
|
|
appState.zoomCountdown = Math.floor(COOLING_PERIOD_MS / 1000);
|
|
appState.zoomCountdownInterval = setInterval(() => {
|
|
appState.zoomCountdown--;
|
|
if (appState.zoomCountdown <= 0) {
|
|
// When countdown finishes, hide panel and clear interval.
|
|
clearInterval(appState.zoomCountdownInterval);
|
|
appState.zoomCountdownInterval = null;
|
|
appState.zoomCountdown = null;
|
|
zoomPanel.style.display = "none";
|
|
} else {
|
|
// Force a redraw of the zoom sketch to show the new countdown value.
|
|
// This call is still needed inside the interval to update the countdown text.
|
|
if (appState.zoomSketchInstance && appState.zoomSketchInstance.updateAndDraw) {
|
|
// Pass empty hoveredItems to show the countdown text.
|
|
appState.zoomSketchInstance.updateAndDraw(
|
|
smoothedMouseX,
|
|
smoothedMouseY,
|
|
[],
|
|
plotScales);
|
|
}
|
|
}
|
|
}, 1000);
|
|
}, 1000); // 1000ms = 1 second delay
|
|
}
|
|
}
|
|
} else {
|
|
// --- START: Cleanup Logic ---
|
|
// When zoom mode is turned off, ensure all timers are cleared.
|
|
if (appState.zoomHideDelayTimeout) {
|
|
clearTimeout(appState.zoomHideDelayTimeout);
|
|
appState.zoomHideDelayTimeout = null;
|
|
}
|
|
if (appState.zoomCountdownInterval) {
|
|
clearInterval(appState.zoomCountdownInterval);
|
|
appState.zoomCountdownInterval = null;
|
|
}
|
|
// --- END: Cleanup Logic ---
|
|
zoomPanel.style.display = "none";
|
|
isFirstFrame = true; // Reset for the next time zoom mode is enabled
|
|
}
|
|
// --- Legend Drawing ---
|
|
// Draw the SNR legend if enabled
|
|
if (toggleSnrColor.checked) {
|
|
p.image(snrLegendBuffer, 10, p.height - snrLegendBuffer.height - 10);
|
|
}
|
|
};
|
|
|
|
// 5. Create the new function to draw the track legend's content
|
|
p.drawTrackLegendToBuffer = function () {
|
|
const b = trackLegendBuffer;
|
|
const localTtcColors = ttcColors(p);
|
|
const isDark = document.documentElement.classList.contains("dark");
|
|
|
|
b.clear();
|
|
b.push();
|
|
|
|
// Set styles based on theme
|
|
const textColor = isDark ? 255 : 0;
|
|
const bgColor = isDark
|
|
? p.color(55, 65, 81, 100)
|
|
: p.color(255, 255, 255, 100);
|
|
|
|
// Draw semi-transparent background for the legend
|
|
b.fill(bgColor);
|
|
b.stroke(1);
|
|
b.strokeWeight(0.25);
|
|
b.rect(0, 0, b.width, b.height, 8); // Rounded corners
|
|
|
|
b.fill(textColor);
|
|
b.textSize(12);
|
|
b.textStyle(b.BOLD);
|
|
b.text("Track Legend", 10, 20);
|
|
|
|
b.textSize(10);
|
|
b.textStyle(b.NORMAL);
|
|
b.strokeWeight(3);
|
|
let yPos = 40;
|
|
|
|
// Legend items
|
|
const legendItems = [
|
|
{ label: "Critical Risk", color: localTtcColors.critical },
|
|
{ label: "High Risk", color: localTtcColors.high },
|
|
{ label: "Medium Risk", color: localTtcColors.medium },
|
|
{ label: "Low Risk", color: localTtcColors.low },
|
|
{ label: "Moving Away", color: localTtcColors.away },
|
|
{ label: "Stationary", color: p.color(34, 139, 34), dashed: true },
|
|
];
|
|
for (const item of legendItems) {
|
|
b.stroke(item.color);
|
|
if (item.dashed) {
|
|
b.drawingContext.setLineDash([3, 3]);
|
|
}
|
|
b.line(15, yPos, 45, yPos);
|
|
b.drawingContext.setLineDash([]); // Reset dash for next items
|
|
b.noStroke();
|
|
b.text(item.label, 55, yPos + 4);
|
|
yPos += 18;
|
|
}
|
|
|
|
b.pop();
|
|
};
|
|
|
|
|
|
|
|
p.windowResized = function () {
|
|
console.log("radarSketch: windowResized triggered!");
|
|
|
|
// Immediately resize the elements that we know are stable.
|
|
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
|
|
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
|
|
trackLegendBuffer = p.createGraphics(120, 120);
|
|
p.drawTrackLegendToBuffer();
|
|
calculatePlotScales();
|
|
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
|
|
|
|
// Defer the call to destroy the zoom canvas.
|
|
if (appState.zoomSketchInstance && appState.isCloseUpMode) {
|
|
setTimeout(() => {
|
|
console.log(
|
|
"radarSketch: Executing deferred call to zoomSketch.handleResize()."
|
|
);
|
|
appState.zoomSketchInstance.handleResize();
|
|
}, 10); // A 10ms delay is slightly more robust than 0.
|
|
}
|
|
|
|
if (appState.vizData) {
|
|
p.redraw();
|
|
}
|
|
};
|
|
|
|
// Function to draw the SNR legend to its buffer
|
|
p.drawSnrLegendToBuffer = function (minV, maxV) {
|
|
// Reference to the SNR legend buffer
|
|
const b = snrLegendBuffer;
|
|
const localSnrColors = snrColors(p);
|
|
b.clear();
|
|
b.push();
|
|
const lx = 10,
|
|
ly = 20,
|
|
lw = 15,
|
|
// Dimensions for the color bar
|
|
lh = 400;
|
|
for (let i = 0; i < lh; i++) {
|
|
const amt = b.map(i, 0, lh, 1, 0);
|
|
let c;
|
|
if (amt < 0.25)
|
|
c = b.lerpColor(localSnrColors.c1, localSnrColors.c2, amt / 0.25);
|
|
else if (amt < 0.5)
|
|
c = b.lerpColor(
|
|
localSnrColors.c2,
|
|
localSnrColors.c3,
|
|
(amt - 0.25) / 0.25
|
|
);
|
|
else if (amt < 0.75)
|
|
c = b.lerpColor(
|
|
localSnrColors.c3,
|
|
localSnrColors.c4,
|
|
(amt - 0.5) / 0.25
|
|
);
|
|
else
|
|
c = b.lerpColor(
|
|
localSnrColors.c4,
|
|
localSnrColors.c5,
|
|
// Interpolate colors based on position
|
|
(amt - 0.75) / 0.25
|
|
);
|
|
b.stroke(c);
|
|
b.line(lx, ly + i, lx + lw, ly + i);
|
|
}
|
|
// Set text color based on theme
|
|
b.fill(document.documentElement.classList.contains("dark") ? 255 : 0);
|
|
b.noStroke();
|
|
b.textSize(10);
|
|
b.textAlign(b.LEFT, b.CENTER);
|
|
// Draw min/max SNR values and label
|
|
b.text(maxV.toFixed(1), lx + lw + 5, ly);
|
|
b.text(minV.toFixed(1), lx + lw + 5, ly + lh);
|
|
b.text("SNR", lx, ly - 10);
|
|
b.pop();
|
|
};
|
|
};
|