diff --git a/steps/src/dom.js b/steps/src/dom.js index 421cb28..995415c 100644 --- a/steps/src/dom.js +++ b/steps/src/dom.js @@ -325,14 +325,14 @@ export function updatePersistentOverlays(currentMediaTime) { radarInfoOverlay.innerHTML = `
Frame: - | Motion State: + | EGO State: | FPS: - | Color Mode: + | Color mode: | Drift: | Δt:
- + `; overlayCache = { @@ -389,9 +389,19 @@ export function updatePersistentOverlays(currentMediaTime) { // --- Draw Optimized Square Block Matrix Graph --- // CONDITIONAL RENDER: Only redraw if frame changed or scale changed significantly - if (appState.currentFrame !== lastDrawnFrame || Math.abs(msPerBlock - lastDrawnScale) > 0.01) { + const dotCanvas = overlayCache.dotCanvas; + let isResized = false; + + if (dotCanvas) { + const clientWidth = dotCanvas.clientWidth; + if (dotCanvas.width !== clientWidth) { + dotCanvas.width = clientWidth; + isResized = true; + } + } + + if (appState.currentFrame !== lastDrawnFrame || Math.abs(msPerBlock - lastDrawnScale) > 0.01 || isResized) { - const dotCanvas = overlayCache.dotCanvas; // Use cached reference if (dotCanvas) { const ctx = dotCanvas.getContext("2d"); const w = dotCanvas.width; @@ -403,9 +413,9 @@ export function updatePersistentOverlays(currentMediaTime) { const stride = blockSize + hGap; // msPerBlock is already set above - // Calculate columns: 600px / 5px = 120 columns. - const totalCols = 140; - const centerCol = 70; + // Calculate columns dynamically based on width + const totalCols = Math.floor(w / stride); + const centerCol = Math.floor(totalCols / 2); ctx.clearRect(0, 0, w, h); @@ -444,7 +454,7 @@ export function updatePersistentOverlays(currentMediaTime) { } } - // Draw Center Indicator (Triangle at column 60) + // Draw Center Indicator (Triangle at center column) const centerX = centerCol * stride + 2; ctx.fillStyle = "#FFFFFF"; ctx.beginPath(); diff --git a/steps/src/drawUtils.js b/steps/src/drawUtils.js index 65ac282..3decfe3 100644 --- a/steps/src/drawUtils.js +++ b/steps/src/drawUtils.js @@ -79,746 +79,774 @@ export const movingColor = (p) => p.color(255, 0, 255); // Magenta * @param {object} plotScales - The calculated scales for plotting. */ export function drawStaticRegionsToBuffer(p, b, plotScales) { - b.clear(); - - // 1. Draw Axes (Grid) - // We pass 'b' as the p5 instance so it draws to the buffer. - // Note: drawAxes applies its own coordinate transformations (translate/scale) internally - // but it expects to start from the top-left relative to the canvas. - // However, inside drawAxes it does: p.translate(5, y * scale)... - // AND logic for flipping. - - // Let's look at how drawAxes is implemented. It pushes/pops and assumes - // it's drawing in SCREEN coordinates (pixels), but then uses plotScales. - // The main draw loop applies: p.translate(width/2, height*0.95); p.scale(1, -1); - // BEFORE calling drawAxes. - - // So 'b' needs to be in that state before we call drawAxes/drawEgoVehicle. - - b.push(); - b.translate(b.width / 2, b.height * 0.95); - b.scale(1, -1); - - // Draw Axes - drawAxes(b, plotScales); // Pass 'b' as the drawing context - - // Draw Ego Vehicle - drawEgoVehicle(b, plotScales); // Pass 'b' as the drawing context - - // 2. Draw Static Regions (Original Logic) - b.stroke(100, 100, 100, 150); - b.strokeWeight(1); - b.drawingContext.setLineDash([8, 8]); - - const a1 = p.radians(30); // Use 'p' for math constants if 'b' lacks them (b usually has them too) - const a2 = p.radians(150); - const len = 70; - - b.line( - 0, - 0, - len * Math.cos(a1) * plotScales.plotScaleX, - len * Math.sin(a1) * plotScales.plotScaleY - ); - b.line( - 0, - 0, - len * Math.cos(a2) * plotScales.plotScaleX, - len * Math.sin(a2) * plotScales.plotScaleY - ); - - b.drawingContext.setLineDash([]); - b.pop(); + try { + b.clear(); + + // 1. Draw Axes (Grid) + // We pass 'b' as the p5 instance so it draws to the buffer. + // Note: drawAxes applies its own coordinate transformations (translate/scale) internally + // but it expects to start from the top-left relative to the canvas. + // However, inside drawAxes it does: p.translate(5, y * scale)... + // AND logic for flipping. + + // Let's look at how drawAxes is implemented. It pushes/pops and assumes + // it's drawing in SCREEN coordinates (pixels), but then uses plotScales. + // The main draw loop applies: p.translate(width/2, height*0.95); p.scale(1, -1); + // BEFORE calling drawAxes. + + // So 'b' needs to be in that state before we call drawAxes/drawEgoVehicle. + + b.push(); + b.translate(b.width / 2, b.height * 0.95); + b.scale(1, -1); + + // Draw Axes + drawAxes(b, plotScales); // Pass 'b' as the drawing context + + // Draw Ego Vehicle + drawEgoVehicle(b, plotScales); // Pass 'b' as the drawing context + + // 2. Draw Static Regions (Original Logic) + b.stroke(100, 100, 100, 150); + b.strokeWeight(1); + b.drawingContext.setLineDash([8, 8]); + + const a1 = p.radians(30); // Use 'p' for math constants if 'b' lacks them (b usually has them too) + const a2 = p.radians(150); + const len = 70; + + b.line( + 0, + 0, + len * Math.cos(a1) * plotScales.plotScaleX, + len * Math.sin(a1) * plotScales.plotScaleY + ); + b.line( + 0, + 0, + len * Math.cos(a2) * plotScales.plotScaleX, + len * Math.sin(a2) * plotScales.plotScaleY + ); + + b.drawingContext.setLineDash([]); + b.pop(); + } catch (error) { + console.error("Error in drawStaticRegionsToBuffer:", error); + } } 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) + try { + 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, - y * plotScales.plotScaleY, + 0, RADAR_X_MAX * plotScales.plotScaleX, - y * plotScales.plotScaleY + 0 ); - // 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, + 0, RADAR_Y_MIN * plotScales.plotScaleY, - x * plotScales.plotScaleX, + 0, 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); + // 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(); + } catch (error) { + console.error("Error in drawAxes:", error); } - p.pop(); } 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; + try { + // 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 + // 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 - c = p.lerpColor( - localSnrColors.c4, - localSnrColors.c5, - (amt - 0.75) / 0.25 - // Interpolate color based on SNR value. + } 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. ); - p.stroke(c); - // Default point color if no specific coloring is applied. - } else { - // --- START: THEME-AWARE POINT COLOR --- - const isDark = document.documentElement.classList.contains("dark"); - // Use a bright lime green for dark mode for better visibility, and the original blue for light mode. - const pointColor = isDark ? p.color(244, 255, 0) : p.color(0, 150, 255); - p.stroke(pointColor); - // --- END: THEME-AWARE POINT COLOR --- + } 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 { + // --- START: THEME-AWARE POINT COLOR --- + const isDark = document.documentElement.classList.contains("dark"); + // Use a bright lime green for dark mode for better visibility, and the original blue for light mode. + const pointColor = isDark ? p.color(244, 255, 0) : p.color(0, 150, 255); + p.stroke(pointColor); + // --- END: THEME-AWARE POINT COLOR --- + } + p.point(pt.x * plotScales.plotScaleX, pt.y * plotScales.plotScaleY); } - p.point(pt.x * plotScales.plotScaleX, pt.y * plotScales.plotScaleY); } + } catch (error) { + console.error("Error in drawPointCloud:", error); } } export function drawTrajectories(p, plotScales) { - const localTtcColors = ttcColors(p); + try { + 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(); + 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; + } - 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) + 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); } - } 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.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) - p.strokeWeight(1.5); - p.drawingContext.setLineDash([]); + // FIND the TTC category from the new timeline + let ttcCategory = null; + if (track.ttcCategoryTimeline) { + const ttcEntry = track.ttcCategoryTimeline.find( + (entry) => entry.frameIdx === lastLog.frameIdx + ); + ttcCategory = ttcEntry ? ttcEntry.ttcCategory : null; // Get the category if found + } - // 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); + 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; + } + } - 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 - ); + 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 --- } - // --- END: New Dynamic Coloring Logic --- - } - p.pop(); // This was the missing pop call for each trajectory loop + p.pop(); // This was the missing pop call for each trajectory loop + } + } catch (error) { + console.error("Error in drawTrajectories:", error); } } 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); - - // Optimization: Batch drawing commands - // We collect all text labels to draw them in a single pass at the end. - // This avoids switching between stroke/fill and push/pop for every track. - const textLabels = []; - - p.push(); - p.strokeWeight(2); - - for (const track of appState.vizData.tracks) { - if (toggleConfirmedOnly.checked && track.isConfirmed === false) continue; - // Robust check for malformed tracks (same as drawTrajectories) - if (!track || !track.historyLog || !Array.isArray(track.historyLog)) continue; - - const log = track.historyLog.find( - (log) => log.frameIdx === appState.currentFrame - ); + try { + 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); + + // Optimization: Batch drawing commands + // We collect all text labels to draw them in a single pass at the end. + // This avoids switching between stroke/fill and push/pop for every track. + const textLabels = []; - 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; - - // --- Draw Marker Shape --- - if (useStationary && log.isStationary === true) { - p.stroke(localStationaryColor); - p.noFill(); - p.rectMode(p.CENTER); - p.square(x, y, size * 1.5); - } else { - let markerColor = p.color(0, 0, 255); - if (useStationary && log.isStationary === false) { - markerColor = localMovingColor; - } - p.stroke(markerColor); - p.line(x - size, y, x + size, y); - p.line(x, y - size, x, y + size); - } + p.push(); + p.strokeWeight(2); + + for (const track of appState.vizData.tracks) { + if (toggleConfirmedOnly.checked && track.isConfirmed === false) continue; + // Robust check for malformed tracks (same as drawTrajectories) + if (!track || !track.historyLog || !Array.isArray(track.historyLog)) continue; + + const log = track.historyLog.find( + (log) => log.frameIdx === appState.currentFrame + ); - // --- Draw Velocity Vector & Collect Text --- - if ( - showDetails && - log.predictedVelocity && - log.predictedVelocity[0] !== null - ) { - const [vx, vy] = log.predictedVelocity; + 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; - // Draw velocity line immediately (shares stroke context) - if (log.isStationary === false) { - // Determine color again (optimization: could be refactored to avoid recalc) - let velocityColor = p.color(255, 0, 255, 200); - if (useStationary) velocityColor = localMovingColor; - - p.stroke(velocityColor); - p.line( - x, - y, - (pos[0] + vx) * plotScales.plotScaleX, - (pos[1] + vy) * plotScales.plotScaleY - ); + // --- Draw Marker Shape --- + if (useStationary && log.isStationary === true) { + p.stroke(localStationaryColor); + p.noFill(); + p.rectMode(p.CENTER); + p.square(x, y, size * 1.5); + } else { + let markerColor = p.color(0, 0, 255); + if (useStationary && log.isStationary === false) { + markerColor = localMovingColor; + } + p.stroke(markerColor); + p.line(x - size, y, x + size, y); + p.line(x, y - size, x, y + size); } - // Defer Text Drawing - const speed = (Math.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}`; - - textLabels.push({ x, y, text }); + // --- Draw Velocity Vector & Collect Text --- + if ( + showDetails && + log.predictedVelocity && + log.predictedVelocity[0] !== null + ) { + const [vx, vy] = log.predictedVelocity; + + // Draw velocity line immediately (shares stroke context) + if (log.isStationary === false) { + // Determine color again (optimization: could be refactored to avoid recalc) + let velocityColor = p.color(255, 0, 255, 200); + if (useStationary) velocityColor = localMovingColor; + + p.stroke(velocityColor); + p.line( + x, + y, + (pos[0] + vx) * plotScales.plotScaleX, + (pos[1] + vy) * plotScales.plotScaleY + ); + } + + // Defer Text Drawing + const speed = (Math.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}`; + + textLabels.push({ x, y, text }); + } } } } - } - p.pop(); // End shape drawing context - - // --- Batch Draw Text --- - if (textLabels.length > 0) { - p.push(); - p.fill(textColor); - p.noStroke(); - p.textSize(12); - // Set alignment once - // Note: we handle the flip manually - - for (const label of textLabels) { - p.push(); - p.translate(label.x + 10, label.y); - p.scale(1, -1); // Flip text back up - p.text(label.text, 0, 0); - p.pop(); - } - p.pop(); + p.pop(); // End shape drawing context + + // --- Batch Draw Text --- + if (textLabels.length > 0) { + p.push(); + p.fill(textColor); + p.noStroke(); + p.textSize(12); + // Set alignment once + // Note: we handle the flip manually + + for (const label of textLabels) { + p.push(); + p.translate(label.x + 10, label.y); + p.scale(1, -1); // Flip text back up + p.text(label.text, 0, 0); + p.pop(); + } + p.pop(); + } + } catch (error) { + console.error("Error in drawTrackMarkers:", error); } } 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 --- - - // --- START: Squared Distance Optimization --- - // We calculate the squared radius once to avoid Math.sqrt() in our loops. - const radiusSq = radius * radius; - // --- END: Squared Distance Optimization --- - 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 + try { + // --- 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 --- + + // --- START: Squared Distance Optimization --- + // We calculate the squared radius once to avoid Math.sqrt() in our loops. + const radiusSq = radius * radius; + // --- END: Squared Distance Optimization --- + const localClusterColors = clusterColors(p); // <-- Get the color palette once + + // ... (Step 1a: Find hovered points - no changes here) ... 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; + // 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; + + // --- START: Squared Distance Optimization --- + // Calculate squared distance to avoid the expensive square root operation. + const dx = mouseX - screenX; + const dy = mouseY - screenY; + if (dx * dx + dy * dy < radiusSq) { + // --- END: Squared Distance Optimization --- + // 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; // --- START: Squared Distance Optimization --- - // Calculate squared distance to avoid the expensive square root operation. const dx = mouseX - screenX; const dy = mouseY - screenY; if (dx * dx + dy * dy < radiusSq) { // --- END: Squared Distance Optimization --- - // Add the index 'i' to the object we push + const color = + cluster.id > 0 + ? localClusterColors[(cluster.id - 1) % localClusterColors.length] + : p.color(128); hoveredItems.push({ - type: "point", - data: pt, + type: "cluster", + data: cluster, screenX, screenY, - index: i, + color: color, }); } } } - } - - // 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; - - // --- START: Squared Distance Optimization --- - const dx = mouseX - screenX; - const dy = mouseY - screenY; - if (dx * dx + dy * dy < radiusSq) { - // --- END: Squared Distance Optimization --- - 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; - // --- START: Squared Distance Optimization --- - const dx = mouseX - screenX; - const dy = mouseY - screenY; - if (dx * dx + dy * dy < radiusSq) { - // --- END: Squared Distance Optimization --- - hoveredItems.push({ - type: "track", - data: currentLog, // Use the log for the current frame - trackId: track.id, - screenX, - screenY, - }); + // 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; + // --- START: Squared Distance Optimization --- + const dx = mouseX - screenX; + const dy = mouseY - screenY; + if (dx * dx + dy * dy < radiusSq) { + // --- END: Squared Distance Optimization --- + 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; - // --- START: Squared Distance Optimization --- - const dx = mouseX - screenX; - const dy = mouseY - screenY; - if (dx * dx + dy * dy < radiusSq) { - // --- END: Squared Distance Optimization --- - hoveredItems.push({ - type: "prediction", - data: currentLog, - 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; + // --- START: Squared Distance Optimization --- + const dx = mouseX - screenX; + const dy = mouseY - screenY; + if (dx * dx + dy * dy < radiusSq) { + // --- END: Squared Distance Optimization --- + 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 - } + // Sort items by their vertical screen position to prevent crossed lines. + hoveredItems.sort((a, b) => a.screenY - b.screenY); - // --- 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 we aren't hovering over anything, draw nothing. + if (hoveredItems.length === 0) { + return hoveredItems; // Return the empty array } - if (infoText) { - infoStrings.push({ text: infoText, color: itemColor }); + + // --- 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; + 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); + 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 highlightColor = p.color(46, 204, 113); - for (const item of hoveredItems) { - p.noFill(); + 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(2); - p.ellipse(item.screenX, item.screenY, 15, 15); - } + 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); + } - 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); - 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(); - const item = hoveredItems[i]; - const lineAnchorY = lineY + lineHeight / 2; - p.stroke(highlightColor); - p.strokeWeight(1); - p.line(lineAnchorX, lineAnchorY, item.screenX, item.screenY); + // Return the list of hovered items for other functions (like the zoom window) to use. + return hoveredItems; + } catch (error) { + console.error("Error in handleCloseUpDisplay:", error); + return []; } - p.pop(); - - // Return the list of hovered items for other functions (like the zoom window) to use. - return hoveredItems; } export function drawCovarianceEllipse( @@ -829,152 +857,168 @@ export function drawCovarianceEllipse( 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(); + try { + // 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(); + } catch (error) { + console.error("Error in drawCovarianceEllipse:", error); + } } 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); + try { + 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); + p.push(); + p.fill(carColor); + p.noStroke(); + p.rectMode(p.CENTER); - const carWidthMeters = 1.5; - const carLengthMeters = 3.5; + const carWidthMeters = 1.5; + const carLengthMeters = 3.5; - const carWidthPixels = carWidthMeters * plotScales.plotScaleX; - const carLengthPixels = carLengthMeters * plotScales.plotScaleY; + const carWidthPixels = carWidthMeters * plotScales.plotScaleX; + const carLengthPixels = carLengthMeters * plotScales.plotScaleY; - p.rect(0, -10, carWidthPixels, carLengthPixels, 5); - p.pop(); + p.rect(0, -10, carWidthPixels, carLengthPixels, 5); + p.pop(); + } catch (error) { + console.error("Error in drawEgoVehicle:", error); + } } 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 + try { + // --- 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 ); - return; // Exit the function if the data is missing for this frame. + + p.pop(); + } catch (error) { + console.error("Error in drawRegionsOfInterest:", error); } - //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(); } export function drawClusterCentroids(p, clustersInput, plotScales) { - if (!clustersInput) { - return; // Do nothing if there's no cluster data - } + try { + 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 --- + // --- 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 - } + if (clusters.length === 0) { + return; // Exit if the resulting array is empty + } - const localClusterColors = clusterColors(p); + 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; + 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); + const color = + cluster.id > 0 + ? localClusterColors[(cluster.id - 1) % localClusterColors.length] + : p.color(128); - p.push(); - p.stroke(color); - p.strokeWeight(1.5); + p.push(); + p.stroke(color); + p.strokeWeight(1.5); - const armLength = 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.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(); + p.pop(); + } } + } catch (error) { + console.error("Error in drawClusterCentroids:", error); } }