// OPAT file handler module for the 4DSTAR Bundle Manager // Extracted from renderer.js to centralize OPAT file parsing and display logic // Import dependencies (these will be injected when integrated) let stateManager, domManager, opatPlotting; // OPAT File Inspector variables let opatFileInput, opatBrowseBtn, opatView, opatCloseBtn; let opatHeaderInfo, opatAllTagsList, opatIndexSelector, opatTablesDisplay, opatTableDataContent; let opatElementsInitialized = false; // Initialize OPAT UI elements function initializeOPATElements() { console.log('[OPAT_HANDLER] initializeOPATElements called, already initialized:', opatElementsInitialized); // Prevent duplicate initialization if (opatElementsInitialized) { console.log('[OPAT_HANDLER] OPAT elements already initialized, skipping...'); return; } opatFileInput = document.getElementById('opat-file-input'); opatBrowseBtn = document.getElementById('opat-browse-btn'); opatView = document.getElementById('opat-view'); opatCloseBtn = document.getElementById('opat-close-btn'); opatHeaderInfo = document.getElementById('opat-header-info'); opatAllTagsList = document.getElementById('opat-all-tags-list'); opatIndexSelector = document.getElementById('opat-index-selector'); opatTablesDisplay = document.getElementById('opat-tables-display'); opatTableDataContent = document.getElementById('opat-table-data-content'); console.log('[OPAT_HANDLER] Found elements:', { opatFileInput: !!opatFileInput, opatBrowseBtn: !!opatBrowseBtn, opatView: !!opatView, opatCloseBtn: !!opatCloseBtn }); // Event listeners if (opatBrowseBtn) { console.log('[OPAT_HANDLER] Adding click listener to browse button'); opatBrowseBtn.addEventListener('click', () => { console.log('[OPAT_HANDLER] Browse button clicked, triggering file input'); if (opatFileInput) { opatFileInput.click(); } else { console.error('[OPAT_HANDLER] File input element not found!'); } }); } if (opatFileInput) { console.log('[OPAT_HANDLER] Adding change listener to file input'); opatFileInput.addEventListener('change', handleOPATFileSelection); } if (opatIndexSelector) { opatIndexSelector.addEventListener('change', handleIndexVectorChange); } if (opatCloseBtn) { opatCloseBtn.addEventListener('click', closeOPATFile); } opatElementsInitialized = true; console.log('[OPAT_HANDLER] OPAT elements initialization complete'); // Initialize OPAT tab navigation initializeOPATTabs(); // Initialize plotting elements if module is available if (opatPlotting) { opatPlotting.initializePlottingElements(); } // Add window resize listener to update table heights window.updateTableHeights = function() { const newHeight = Math.max(300, window.innerHeight - 450); // Target the main table containers const containers = document.querySelectorAll('.opat-table-container'); containers.forEach((container, index) => { container.style.setProperty('height', newHeight + 'px', 'important'); }); }; window.addEventListener('resize', window.updateTableHeights); } // Initialize OPAT tab navigation function initializeOPATTabs() { // Use the correct class name that matches the HTML const opatTabLinks = document.querySelectorAll('#opat-view .tab-link'); const opatTabPanes = document.querySelectorAll('#opat-view .tab-pane'); console.log(`[OPAT_HANDLER] Found ${opatTabLinks.length} OPAT tab links`); opatTabLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetTab = link.dataset.tab; console.log(`[OPAT_HANDLER] Tab clicked: ${targetTab}`); // Update active states opatTabLinks.forEach(l => l.classList.remove('active')); opatTabPanes.forEach(p => { p.classList.remove('active'); p.classList.add('hidden'); }); link.classList.add('active'); const targetPane = document.getElementById(targetTab); if (targetPane) { targetPane.classList.add('active'); targetPane.classList.remove('hidden'); console.log(`[OPAT_HANDLER] Switched to tab: ${targetTab}`); } }); }); } // Reset OPAT viewer state function resetOPATViewerState() { if (opatHeaderInfo) opatHeaderInfo.innerHTML = ''; if (opatAllTagsList) opatAllTagsList.innerHTML = ''; if (opatIndexSelector) opatIndexSelector.innerHTML = ''; if (opatTablesDisplay) opatTablesDisplay.innerHTML = ''; if (opatTableDataContent) opatTableDataContent.innerHTML = ''; // Reset plotting state if module is available if (opatPlotting) { opatPlotting.resetPlottingState(); } } // Handle OPAT file selection async function handleOPATFileSelection(event) { console.log('[OPAT_HANDLER] ===== FILE SELECTION EVENT TRIGGERED ====='); console.log('[OPAT_HANDLER] Event target:', event.target); console.log('[OPAT_HANDLER] Files array:', event.target.files); console.log('[OPAT_HANDLER] Number of files:', event.target.files ? event.target.files.length : 0); const file = event.target.files[0]; if (!file) { console.log('[OPAT_HANDLER] No file selected - event fired but no file found'); return; } console.log('[OPAT_HANDLER] File selected:', { name: file.name, size: file.size, type: file.type, lastModified: new Date(file.lastModified) }); try { console.log('[OPAT_HANDLER] Starting file processing...'); // Reset the viewer state console.log('[OPAT_HANDLER] Resetting viewer state...'); resetOPATViewerState(); // Show the OPAT view first to ensure UI is visible console.log('[OPAT_HANDLER] Showing OPAT view...'); domManager.showView('opat-view'); // Read and parse the file console.log('[OPAT_HANDLER] Reading file as ArrayBuffer...'); const arrayBuffer = await file.arrayBuffer(); console.log('[OPAT_HANDLER] File read successfully, arrayBuffer size:', arrayBuffer.byteLength); // Check if parseOPAT is available console.log('[OPAT_HANDLER] Checking parseOPAT availability...'); console.log('[OPAT_HANDLER] typeof parseOPAT:', typeof parseOPAT); console.log('[OPAT_HANDLER] window.parseOPAT:', typeof window.parseOPAT); if (typeof parseOPAT === 'undefined' && typeof window.parseOPAT === 'undefined') { throw new Error('parseOPAT function is not available. Make sure opatParser.js is loaded.'); } // Use global parseOPAT if local one is undefined const parseFunction = typeof parseOPAT !== 'undefined' ? parseOPAT : window.parseOPAT; console.log('[OPAT_HANDLER] Using parse function:', typeof parseFunction); console.log('[OPAT_HANDLER] Calling parseOPAT...'); const currentOPATFile = parseFunction(arrayBuffer); console.log('[OPAT_HANDLER] Parse result:', currentOPATFile ? 'SUCCESS' : 'FAILED'); console.log('[OPAT_HANDLER] Parsed file object:', currentOPATFile); if (currentOPATFile) { console.log('[OPAT_HANDLER] Setting file in state manager...'); stateManager.setOPATFile(currentOPATFile); // Display file information console.log('[OPAT_HANDLER] Displaying file information...'); displayOPATFileInfo(); displayAllTableTags(); populateIndexSelector(); console.log('[OPAT_HANDLER] ===== OPAT FILE LOADED SUCCESSFULLY ====='); } else { console.error('[OPAT_HANDLER] parseOPAT returned null/undefined'); domManager.showModal('Error', 'Failed to parse OPAT file. Please check the file format.'); } } catch (error) { console.error('[OPAT_HANDLER] ===== ERROR IN FILE PROCESSING ====='); console.error('[OPAT_HANDLER] Error details:', error); console.error('[OPAT_HANDLER] Error stack:', error.stack); domManager.showModal('Error', `Failed to load OPAT file: ${error.message}`); } finally { console.log('[OPAT_HANDLER] Cleaning up file input...'); // Clear the file input to prevent issues with reopening the same file if (event.target) { event.target.value = ''; console.log('[OPAT_HANDLER] File input cleared'); } console.log('[OPAT_HANDLER] ===== FILE SELECTION HANDLER COMPLETE ====='); } } // Open OPAT file from file path (for file associations) async function openOpatFromPath(filePath) { if (!filePath) { console.log('[OPAT_HANDLER] openOpatFromPath: No file path provided'); return; } try { console.log('[OPAT_HANDLER] Opening OPAT file from path:', filePath); // Ensure OPAT UI elements are initialized console.log('[OPAT_HANDLER] Initializing OPAT UI elements...'); initializeOPATElements(); initializeOPATTabs(); // Reset the viewer state resetOPATViewerState(); // Show the OPAT view first to ensure UI is visible console.log('[OPAT_HANDLER] Showing OPAT view...'); domManager.showView('opat-view'); // Read the file using Node.js fs const fs = require('fs'); console.log('[OPAT_HANDLER] Reading file from disk...'); const fileBuffer = fs.readFileSync(filePath); const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength); console.log('[OPAT_HANDLER] File read successfully, arrayBuffer size:', arrayBuffer.byteLength); // Parse the OPAT file console.log('[OPAT_HANDLER] Parsing OPAT file...'); if (typeof parseOPAT === 'undefined') { throw new Error('parseOPAT function is not available. Make sure opatParser.js is loaded.'); } const currentOPATFile = parseOPAT(arrayBuffer); console.log('[OPAT_HANDLER] Parse result:', currentOPATFile ? 'SUCCESS' : 'FAILED'); if (currentOPATFile) { console.log('[OPAT_HANDLER] Setting file in state manager...'); stateManager.setOPATFile(currentOPATFile); // Display file information console.log('[OPAT_HANDLER] Displaying file information...'); displayOPATFileInfo(); displayAllTableTags(); populateIndexSelector(); console.log('[OPAT_HANDLER] OPAT file opened successfully via file association'); } else { console.error('[OPAT_HANDLER] parseOPAT returned null/undefined for file association'); throw new Error('Failed to parse OPAT file. Please check the file format.'); } } catch (error) { console.error('[OPAT_HANDLER] Error opening OPAT file via file association:', error); domManager.showModal('Error', `Failed to open OPAT file: ${error.message}`); } } // Display OPAT file information function displayOPATFileInfo() { console.log('[OPAT_HANDLER] displayOPATFileInfo called'); const currentOPATFile = stateManager.getOPATFile(); console.log('[OPAT_HANDLER] Current OPAT file from state:', currentOPATFile); if (!currentOPATFile) { console.error('[OPAT_HANDLER] No OPAT file in state manager!'); return; } console.log('[OPAT_HANDLER] opatHeaderInfo element:', opatHeaderInfo); console.log('[OPAT_HANDLER] opatHeaderInfo exists:', !!opatHeaderInfo); if (!opatHeaderInfo) { console.error('[OPAT_HANDLER] opatHeaderInfo element not found! Re-initializing...'); opatHeaderInfo = document.getElementById('opat-header-info'); console.log('[OPAT_HANDLER] After re-init, opatHeaderInfo:', !!opatHeaderInfo); } const header = currentOPATFile.header; console.log('[OPAT_HANDLER] Header object:', header); const headerHTML = `
Magic: ${header.magic}
Version: ${header.version}
Number of Tables: ${header.numTables}
Header Size: ${header.headerSize} bytes
Index Offset: ${header.indexOffset}
Creation Date: ${header.creationDate}
Source Info: ${header.sourceInfo}
Comment: ${header.comment || 'None'}
Number of Indices: ${header.numIndex}
Hash Precision: ${header.hashPrecision}
Table not found.
'; return; } let html = `Dimensions: ${table.N_R} rows × ${table.N_C} columns × ${table.m_vsize} values per cell
`; if (table.N_R > 0 && table.N_C > 0) { if (table.m_vsize === 0 || table.data.length === 0) { html += 'Note: This table has no data values (m_vsize = 0 or empty data array).
'; html += 'The table structure exists but contains no numerical data to display.
'; } else { // Add show all/show less toggle buttons if (table.N_R > 50) { html += '| '; for (let c = 0; c < table.N_C; c++) { html += ` | ${table.columnValues[c].toFixed(3)} | `; } html += '|
|---|---|---|
| ${table.rowValues[r].toFixed(3)} | `; for (let c = 0; c < table.N_C; c++) { try { const value = table.getValue(r, c, 0); // Get first value in cell html += `${value.toFixed(6)} | `; } catch (error) { html += `N/A | `; } } html += '
Showing first 50 rows of ${table.N_R} total rows.
`; } else if (showAll && table.N_R > 50) { html += `Showing all ${table.N_R} rows.
`; } } } else { html += 'No data to display.
'; } opatTableDataContent.innerHTML = html; // Add event listeners for show all/show less buttons const showAllBtns = opatTableDataContent.querySelectorAll('.show-all-btn'); const showLessBtns = opatTableDataContent.querySelectorAll('.show-less-btn'); showAllBtns.forEach(btn => { btn.addEventListener('click', () => { const tag = btn.dataset.tag; console.log(`[OPAT_HANDLER] Show all rows clicked for tag: ${tag}`); const currentIndexValue = opatIndexSelector.value; if (currentIndexValue && stateManager.getOPATFile()) { const tableData = stateManager.getOPATFile().cards.get(currentIndexValue).tableData.get(tag); displayTableData(tableData, tag, true); } }); }); showLessBtns.forEach(btn => { btn.addEventListener('click', () => { const tag = btn.dataset.tag; console.log(`[OPAT_HANDLER] Show less rows clicked for tag: ${tag}`); const currentIndexValue = opatIndexSelector.value; if (currentIndexValue && stateManager.getOPATFile()) { const tableData = stateManager.getOPATFile().cards.get(currentIndexValue).tableData.get(tag); displayTableData(tableData, tag, false); } }); }); // Auto-switch to Data Explorer tab when displaying data const explorerTab = document.querySelector('[data-tab="opat-explorer-tab"]'); if (explorerTab) { explorerTab.click(); } // Update table heights after table is rendered setTimeout(() => { if (window.updateTableHeights) { window.updateTableHeights(); } }, 50); } // Close OPAT file function closeOPATFile() { stateManager.clearOPATFile(); resetOPATViewerState(); // Reset file input if (opatFileInput) { opatFileInput.value = ''; } // Hide OPAT view and show appropriate home screen hideAllViews(); showCategoryHomeScreen('opat'); } // Helper function to hide all views function hideAllViews() { const views = [ 'welcome-screen', 'libplugin-home', 'opat-home', 'libconstants-home', 'serif-home', 'opat-view', 'libplugin-view' ]; views.forEach(viewId => { const view = document.getElementById(viewId); if (view) view.classList.add('hidden'); }); } // Show appropriate home screen based on selected category function showCategoryHomeScreen(category) { hideAllViews(); const viewMap = { 'home': 'welcome-screen', 'libplugin': 'libplugin-home', 'opat': 'opat-home', 'libconstants': 'libconstants-home', 'serif': 'serif-home' }; const viewId = viewMap[category] || 'welcome-screen'; const view = document.getElementById(viewId); if (view) view.classList.remove('hidden'); } // Initialize dependencies (called when module is loaded) function initializeDependencies(deps) { stateManager = deps.stateManager; domManager = deps.domManager; opatPlotting = deps.opatPlotting; } module.exports = { initializeDependencies, initializeOPATElements, initializeOPATTabs, resetOPATViewerState, handleOPATFileSelection, openOpatFromPath, displayOPATFileInfo, displayAllTableTags, populateIndexSelector, handleIndexVectorChange, displayTableData, closeOPATFile, hideAllViews, showCategoryHomeScreen };