|
|
|
@ -4,7 +4,8 @@ import { appState } from './state.js'; |
|
|
|
import { throttle } from './utils.js'; |
|
|
|
import { |
|
|
|
canvasContainer, |
|
|
|
explorerBtn |
|
|
|
explorerBtn, |
|
|
|
mainContent |
|
|
|
} from './dom.js'; // Import the DOM elements we need to listen to
|
|
|
|
|
|
|
|
// --- DOM Elements (Internal to this module) ---
|
|
|
|
@ -16,14 +17,17 @@ const plotBtn = document.getElementById('plot-selected-btn'); |
|
|
|
const tabs = { |
|
|
|
tree: { btn: document.getElementById('tab-btn-tree'), panel: document.getElementById('tab-panel-tree') }, |
|
|
|
grid: { btn: document.getElementById('tab-btn-grid'), panel: document.getElementById('tab-panel-grid') }, |
|
|
|
trackGrid: { btn: document.getElementById('tab-btn-track-grid'), panel: document.getElementById('tab-panel-track-grid') }, |
|
|
|
plot: { btn: document.getElementById('tab-btn-plot'), panel: document.getElementById('tab-panel-plot') }, |
|
|
|
}; |
|
|
|
|
|
|
|
const gridDiv = document.getElementById('data-grid'); |
|
|
|
const trackGridDiv = document.getElementById('track-data-grid'); |
|
|
|
const chartCanvas = document.getElementById('data-chart'); |
|
|
|
|
|
|
|
// --- Module-Local State ---
|
|
|
|
let gridApi = null; |
|
|
|
let trackGridApi = null; |
|
|
|
let chartInstance = null; |
|
|
|
let currentGridData = null; |
|
|
|
|
|
|
|
@ -35,9 +39,42 @@ const gridOptions = { |
|
|
|
sortable: true, |
|
|
|
filter: true, |
|
|
|
resizable: true, |
|
|
|
// --- START: Set a default width for all columns ---
|
|
|
|
width: 100, // Keep width at 100 as requested
|
|
|
|
// --- END: Set a default width ---
|
|
|
|
}, |
|
|
|
// --- START: Define a specific column type for numbers ---
|
|
|
|
// This allows us to apply special formatting only to numeric columns.
|
|
|
|
columnTypes: { |
|
|
|
numberColumn: { |
|
|
|
// --- START: Add column-specific number formatting ---
|
|
|
|
// This formatter checks the column ID and applies the correct precision.
|
|
|
|
valueFormatter: params => { |
|
|
|
const { value, colDef } = params; |
|
|
|
// Do nothing if value is not a number or is an integer (like 'index')
|
|
|
|
if (typeof value !== 'number' || value === null || Number.isInteger(value)) { |
|
|
|
return value; |
|
|
|
} |
|
|
|
|
|
|
|
// Apply formatting based on the column's field name
|
|
|
|
switch (colDef.field) { |
|
|
|
case 'snr': |
|
|
|
return value.toFixed(2); // SNR gets 2 decimal places
|
|
|
|
default: |
|
|
|
return value.toFixed(4); // All other numbers get 4 decimal places
|
|
|
|
} |
|
|
|
} |
|
|
|
// --- END: Add column-specific number formatting ---
|
|
|
|
} |
|
|
|
} |
|
|
|
// --- END: Define a specific column type for numbers ---
|
|
|
|
}; |
|
|
|
|
|
|
|
const trackGridOptions = { |
|
|
|
...gridOptions |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// --- Chart.js Configuration ---
|
|
|
|
function createChart(data, label) { |
|
|
|
if (chartInstance) { |
|
|
|
@ -72,7 +109,7 @@ function hideExplorer() { |
|
|
|
panel.classList.add('hidden'); |
|
|
|
} |
|
|
|
|
|
|
|
function switchTab(targetTab) { |
|
|
|
/* function switchTab(targetTab) { |
|
|
|
Object.values(tabs).forEach(tab => { |
|
|
|
tab.panel.classList.add('hidden'); |
|
|
|
tab.btn.classList.remove('border-blue-500', 'text-gray-900', 'dark:text-white'); |
|
|
|
@ -83,6 +120,25 @@ function switchTab(targetTab) { |
|
|
|
tabs[targetTab].btn.classList.add('border-blue-500', 'text-gray-900', 'dark:text-white'); |
|
|
|
tabs[targetTab].btn.classList.remove('text-gray-500', 'dark:text-gray-400'); |
|
|
|
|
|
|
|
footer.classList.toggle('hidden', targetTab !== 'grid'); |
|
|
|
} */ |
|
|
|
// In src/dataExplorer.js
|
|
|
|
|
|
|
|
function switchTab(targetTab) { |
|
|
|
Object.values(tabs).forEach(tab => { |
|
|
|
tab.panel.classList.add('hidden'); |
|
|
|
// Remove active classes (border and color)
|
|
|
|
tab.btn.classList.remove('border-b-2', 'border-blue-500'); |
|
|
|
// Add inactive classes (gray text)
|
|
|
|
tab.btn.classList.add('text-gray-500', 'dark:text-gray-400'); |
|
|
|
}); |
|
|
|
|
|
|
|
tabs[targetTab].panel.classList.remove('hidden'); |
|
|
|
// Add active classes (border and color)
|
|
|
|
tabs[targetTab].btn.classList.add('border-b-2', 'border-blue-500'); |
|
|
|
// Remove inactive classes (gray text)
|
|
|
|
tabs[targetTab].btn.classList.remove('text-gray-500', 'dark:text-gray-400'); |
|
|
|
|
|
|
|
footer.classList.toggle('hidden', targetTab !== 'grid'); |
|
|
|
} |
|
|
|
|
|
|
|
@ -101,32 +157,108 @@ function updateExplorer() { |
|
|
|
const frame = appState.vizData.radarFrames[appState.currentFrame]; |
|
|
|
if (!frame) return; |
|
|
|
|
|
|
|
// --- START: Correctly gather track data for the current frame ---
|
|
|
|
// We iterate through all tracks and find the history log entry for the current frame.
|
|
|
|
const tracksForCurrentFrame = appState.vizData.tracks |
|
|
|
.map(track => { |
|
|
|
const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame); |
|
|
|
// Return a new object combining track ID with its log for this frame, if it exists.
|
|
|
|
return log ? { trackId: track.id, ...log } : null; |
|
|
|
}) |
|
|
|
.filter(Boolean); // Filter out any null entries for tracks not present in this frame.
|
|
|
|
// --- END: Correctly gather track data for the current frame ---
|
|
|
|
|
|
|
|
tabs.tree.panel.innerHTML = ''; |
|
|
|
tabs.tree.panel.appendChild(createTreeView({ |
|
|
|
currentFrame: appState.currentFrame, |
|
|
|
frameData: frame |
|
|
|
currentFrame: appState.currentFrame + 1, |
|
|
|
frameData: frame, |
|
|
|
trackData: tracksForCurrentFrame // Use the newly created array
|
|
|
|
})); |
|
|
|
|
|
|
|
// --- START: Auto-update Point Cloud Grid ---
|
|
|
|
displayInGrid(frame.pointCloud, `${appState.currentFrame + 1}`); |
|
|
|
// --- END: Auto-update Point Cloud Grid ---
|
|
|
|
displayTracksInGrid(tracksForCurrentFrame); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
function displayInGrid(data, title) { |
|
|
|
if (!data || data.length === 0 || !gridApi) return; |
|
|
|
if (!Array.isArray(data) || data.length === 0 || !gridApi) return; |
|
|
|
|
|
|
|
// --- START: Add index and prepare data ---
|
|
|
|
// We map the original data to a new array, adding an 'index' property to each object.
|
|
|
|
const indexedData = data.map((row, index) => ({ |
|
|
|
index: index, |
|
|
|
...row |
|
|
|
})); |
|
|
|
currentGridData = indexedData; // Store the new indexed data
|
|
|
|
// --- END: Add index and prepare data ---
|
|
|
|
|
|
|
|
currentGridData = data; |
|
|
|
// Auto-generate columns from the first data object
|
|
|
|
const columns = Object.keys(data[0]).map(key => ({ |
|
|
|
// --- START: More robust column generation ---
|
|
|
|
// Auto-generate columns, and assign a 'type' if the data is numeric.
|
|
|
|
const columns = Object.keys(indexedData[0]).map(key => ({ |
|
|
|
field: key, |
|
|
|
headerName: key, // Set header name
|
|
|
|
sortable: true, // Ensure all generated columns are sortable
|
|
|
|
filter: true, // Ensure all generated columns are filterable
|
|
|
|
// If the first row's value for this key is a number, assign our custom number type.
|
|
|
|
type: typeof indexedData[0][key] === 'number' ? 'numberColumn' : undefined |
|
|
|
})); |
|
|
|
// --- END: More robust column generation ---
|
|
|
|
|
|
|
|
gridApi.setGridOption('columnDefs', columns); |
|
|
|
gridApi.setGridOption('rowData', data); |
|
|
|
gridApi.setGridOption('rowData', indexedData); |
|
|
|
|
|
|
|
// --- START: Apply default sort ---
|
|
|
|
// After setting the data, apply a sort model to sort by the 'index' column ascending.
|
|
|
|
gridApi.applyColumnState({ state: [{ colId: 'index', sort: 'asc' }] }); |
|
|
|
// --- END: Apply default sort ---
|
|
|
|
|
|
|
|
tabs.grid.btn.textContent = `Point Cloud: Frame ${title}`; |
|
|
|
} |
|
|
|
|
|
|
|
function displayTracksInGrid(trackData) { |
|
|
|
if (!trackGridApi || !Array.isArray(trackData) || trackData.length === 0) { |
|
|
|
if (trackGridApi) trackGridApi.setGridOption('rowData', []); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// --- START: Update Track Grid Title ---
|
|
|
|
tabs.trackGrid.btn.textContent = `Track Grid: Frame ${appState.currentFrame + 1}`; |
|
|
|
// --- END: Update Track Grid Title ---
|
|
|
|
|
|
|
|
// --- START: Build a complete set of columns from ALL tracks ---
|
|
|
|
// Create a Set of all unique keys from all track objects in the current frame.
|
|
|
|
// This prevents missing columns if the first track has fewer properties than others.
|
|
|
|
const allKeys = new Set(); |
|
|
|
trackData.forEach(track => { |
|
|
|
Object.keys(track).forEach(key => allKeys.add(key)); |
|
|
|
}); |
|
|
|
|
|
|
|
// --- START: Fix for column type detection ---
|
|
|
|
const columns = Array.from(allKeys).map(key => ({ |
|
|
|
field: key, |
|
|
|
headerName: key, |
|
|
|
// Find the first track that has a non-null value for this key to determine its type.
|
|
|
|
// This is much more reliable than only checking the first track in the list.
|
|
|
|
type: typeof trackData.find(t => t[key] !== null && t[key] !== undefined)?.[key] === 'number' ? 'numberColumn' : undefined, |
|
|
|
valueFormatter: params => { |
|
|
|
if (Array.isArray(params.value)) { |
|
|
|
// --- START: Robust Array Formatting ---
|
|
|
|
// Check if the item 'v' is a number before calling toFixed.
|
|
|
|
// If it's not a number (e.g., it's another array), stringify it.
|
|
|
|
return `[${params.value.map(v => (typeof v === 'number' && v !== null) ? v.toFixed(3) : JSON.stringify(v)).join(', ')}]`; |
|
|
|
// --- END: Robust Array Formatting ---
|
|
|
|
} |
|
|
|
return params.value; |
|
|
|
} |
|
|
|
})); |
|
|
|
// --- END: Fix for column type detection ---
|
|
|
|
// --- END: Build a complete set of columns from ALL tracks ---
|
|
|
|
|
|
|
|
tabs.grid.btn.textContent = `Grid View: ${title}`; |
|
|
|
switchTab('grid'); |
|
|
|
trackGridApi.setGridOption('columnDefs', columns); |
|
|
|
trackGridApi.setGridOption('rowData', trackData); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// --- START: New Robust Update Logic ---
|
|
|
|
let throttleTimer = null; |
|
|
|
let debounceTimer = null; |
|
|
|
@ -145,7 +277,7 @@ export function throttledUpdateExplorer() { |
|
|
|
if (!throttleTimer) { |
|
|
|
updateExplorer(); // ...execute the update immediately.
|
|
|
|
// Then, set a cool-down timer to prevent another immediate execution.
|
|
|
|
throttleTimer = setTimeout(() => { throttleTimer = null; }, 400); // 400ms throttle
|
|
|
|
throttleTimer = setTimeout(() => { throttleTimer = null; }, 100); // 100ms throttle for snappy display
|
|
|
|
} |
|
|
|
|
|
|
|
// Schedule a final, debounced update for after the interactions stop.
|
|
|
|
@ -153,6 +285,112 @@ export function throttledUpdateExplorer() { |
|
|
|
} |
|
|
|
// --- END: New Robust Update Logic ---
|
|
|
|
|
|
|
|
// --- START: Resizable and Draggable Panel Logic ---
|
|
|
|
function makePanelInteractive(panel) { |
|
|
|
const header = document.getElementById('data-explorer-header'); |
|
|
|
const resizers = panel.querySelectorAll('.resizer'); |
|
|
|
const minWidth = 400; |
|
|
|
const minHeight = 300; |
|
|
|
|
|
|
|
let original_width = 0; |
|
|
|
let original_height = 0; |
|
|
|
let original_x = 0; |
|
|
|
let original_y = 0; |
|
|
|
let original_mouse_x = 0; |
|
|
|
let original_mouse_y = 0; |
|
|
|
|
|
|
|
// --- Dragging Logic ---
|
|
|
|
header.addEventListener('mousedown', (e) => { |
|
|
|
e.preventDefault(); |
|
|
|
original_x = panel.offsetLeft; |
|
|
|
original_y = panel.offsetTop; |
|
|
|
original_mouse_x = e.pageX; |
|
|
|
original_mouse_y = e.pageY; |
|
|
|
document.body.classList.add('dragging'); |
|
|
|
window.addEventListener('mousemove', dragPanel); |
|
|
|
window.addEventListener('mouseup', stopDrag); |
|
|
|
}); |
|
|
|
|
|
|
|
function dragPanel(e) { |
|
|
|
const dx = e.pageX - original_mouse_x; |
|
|
|
const dy = e.pageY - original_mouse_y; |
|
|
|
panel.style.left = `${original_x + dx}px`; |
|
|
|
panel.style.top = `${original_y + dy}px`; |
|
|
|
} |
|
|
|
|
|
|
|
function stopDrag() { |
|
|
|
document.body.classList.remove('dragging'); |
|
|
|
window.removeEventListener('mousemove', dragPanel); |
|
|
|
window.removeEventListener('mouseup', stopDrag); |
|
|
|
} |
|
|
|
|
|
|
|
// --- Resizing Logic ---
|
|
|
|
resizers.forEach(resizer => { |
|
|
|
resizer.addEventListener('mousedown', (e) => { |
|
|
|
e.preventDefault(); |
|
|
|
original_width = parseFloat(getComputedStyle(panel, null).getPropertyValue('width').replace('px', '')); |
|
|
|
original_height = parseFloat(getComputedStyle(panel, null).getPropertyValue('height').replace('px', '')); |
|
|
|
original_x = panel.getBoundingClientRect().left; |
|
|
|
original_y = panel.getBoundingClientRect().top; |
|
|
|
original_mouse_x = e.pageX; |
|
|
|
original_mouse_y = e.pageY; |
|
|
|
|
|
|
|
const resizeFunc = (event) => resizePanel(event, resizer.classList); |
|
|
|
|
|
|
|
document.body.classList.add('resizing'); |
|
|
|
window.addEventListener('mousemove', resizeFunc); |
|
|
|
window.addEventListener('mouseup', () => { |
|
|
|
document.body.classList.remove('resizing'); |
|
|
|
window.removeEventListener('mousemove', resizeFunc); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
function resizePanel(e, direction) { |
|
|
|
// --- START: Fix for Resizing Logic ---
|
|
|
|
// The logic is updated to handle corners correctly by checking for 't', 'b', 'l', 'r' substrings.
|
|
|
|
// This allows a corner like 'resizer-br' to trigger both bottom and right resizing logic.
|
|
|
|
if (direction.toString().includes('r')) { // Check for right edge
|
|
|
|
const width = original_width + (e.pageX - original_mouse_x); |
|
|
|
if (width > minWidth) panel.style.width = `${width}px`; |
|
|
|
} |
|
|
|
if (direction.toString().includes('b')) { // Check for bottom edge
|
|
|
|
const height = original_height + (e.pageY - original_mouse_y); |
|
|
|
if (height > minHeight) panel.style.height = `${height}px`; |
|
|
|
} |
|
|
|
if (direction.toString().includes('l')) { // Check for left edge
|
|
|
|
const newWidth = original_width - (e.pageX - original_mouse_x); |
|
|
|
if (newWidth > minWidth) { |
|
|
|
panel.style.width = `${newWidth}px`; |
|
|
|
panel.style.left = `${original_x + (e.pageX - original_mouse_x)}px`; |
|
|
|
} |
|
|
|
} |
|
|
|
if (direction.toString().includes('t')) { // Check for top edge
|
|
|
|
const newHeight = original_height - (e.pageY - original_mouse_y); |
|
|
|
if (newHeight > minHeight) { |
|
|
|
panel.style.height = `${newHeight}px`; |
|
|
|
panel.style.top = `${original_y + (e.pageY - original_mouse_y)}px`; |
|
|
|
} |
|
|
|
} |
|
|
|
// --- END: Fix for Resizing Logic ---
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function initializePanelPosition(panel) { |
|
|
|
// Remove Tailwind classes that conflict with dynamic positioning/sizing
|
|
|
|
panel.classList.remove('bottom-24', 'right-4', 'w-full', 'max-w-2xl', 'h-1/2'); |
|
|
|
|
|
|
|
// Set initial size and position with inline styles
|
|
|
|
const initialWidth = 896; // Corresponds to max-w-2xl
|
|
|
|
const initialHeight = window.innerHeight / 2; |
|
|
|
|
|
|
|
panel.style.width = `${initialWidth}px`; |
|
|
|
panel.style.height = `${initialHeight}px`; |
|
|
|
panel.style.top = `${window.innerHeight - initialHeight - 96}px`; // 96px is roughly bottom-24
|
|
|
|
panel.style.left = `${window.innerWidth - initialWidth - 16}px`; // 16px is right-4
|
|
|
|
} |
|
|
|
// --- END: Resizable and Draggable Panel Logic ---
|
|
|
|
|
|
|
|
// --- Initialization Function (The file's only export) ---
|
|
|
|
|
|
|
|
export function initializeDataExplorer() { |
|
|
|
@ -161,6 +399,14 @@ export function initializeDataExplorer() { |
|
|
|
gridApi = agGrid.createGrid(gridDiv, gridOptions); |
|
|
|
} |
|
|
|
|
|
|
|
if (!trackGridApi) { |
|
|
|
trackGridApi = agGrid.createGrid(trackGridDiv, trackGridOptions); |
|
|
|
} |
|
|
|
|
|
|
|
// --- START: Make panel interactive ---
|
|
|
|
initializePanelPosition(panel); |
|
|
|
makePanelInteractive(panel); |
|
|
|
// --- END: Make panel interactive ---
|
|
|
|
// --- Wire up all event listeners ---
|
|
|
|
|
|
|
|
// Toggle panel visibility
|
|
|
|
@ -198,21 +444,6 @@ export function initializeDataExplorer() { |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// Main canvas click listener
|
|
|
|
canvasContainer.addEventListener('click', () => { |
|
|
|
if (!appState.vizData) return; |
|
|
|
|
|
|
|
const currentFrameData = appState.vizData.radarFrames[appState.currentFrame]; |
|
|
|
if (currentFrameData && currentFrameData.pointCloud) { |
|
|
|
// Send point cloud data to the grid
|
|
|
|
displayInGrid(currentFrameData.pointCloud, `Frame ${appState.currentFrame} - Point Cloud`); |
|
|
|
// Show the explorer if it's hidden
|
|
|
|
if (panel.classList.contains('hidden')) { |
|
|
|
showExplorer(); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// Keyboard shortcut listener
|
|
|
|
document.addEventListener("keydown", (event) => { |
|
|
|
// Ignore if typing in an input
|
|
|
|
|