/** * Export Manager for Gallery * Handles exporting selected plots to merged PDF */ export class ExportManager { constructor() { this.selectedPlots = new Set(); this.maxPlots = 4; this.init(); } init() { this.createExportButton(); this.bindEvents(); } /** * Create the export button in the floating buttons section */ createExportButton() { const floatingButtons = document.querySelector('.floating-buttons'); if (!floatingButtons) return; const exportBtn = document.createElement('button'); exportBtn.className = 'floating-btn export-btn'; exportBtn.id = 'exportBtn'; exportBtn.title = 'Export Selected Plots (Ctrl+E)'; exportBtn.innerHTML = '📄'; exportBtn.style.display = 'none'; // Hidden by default exportBtn.onclick = () => this.exportSelectedPlots(); floatingButtons.appendChild(exportBtn); // Add selection counter const selectionCounter = document.createElement('div'); selectionCounter.className = 'selection-counter'; selectionCounter.id = 'selectionCounter'; selectionCounter.style.display = 'none'; selectionCounter.innerHTML = '0/4 selected'; floatingButtons.appendChild(selectionCounter); } /** * Bind events for plot selection */ bindEvents() { // Add selection mode toggle document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'e') { e.preventDefault(); this.toggleSelectionMode(); } if (e.key === 'Escape') { this.clearSelection(); } }); // Add selection handlers to existing plots this.addSelectionHandlers(); } /** * Add selection handlers to all plot items */ addSelectionHandlers() { const plotItems = document.querySelectorAll('.grid-item'); plotItems.forEach(item => this.addSelectionHandler(item)); } /** * Add selection handler to a single plot item */ addSelectionHandler(item) { // Create selection overlay const overlay = document.createElement('div'); overlay.className = 'selection-overlay'; overlay.innerHTML = `
`; item.appendChild(overlay); // Add click handler for selection overlay.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.togglePlotSelection(item); }); } /** * Toggle selection mode */ toggleSelectionMode() { const body = document.body; const isSelectionMode = body.classList.contains('selection-mode'); if (isSelectionMode) { this.exitSelectionMode(); } else { this.enterSelectionMode(); } } /** * Enter selection mode */ enterSelectionMode() { document.body.classList.add('selection-mode'); document.getElementById('exportBtn').style.display = 'block'; document.getElementById('selectionCounter').style.display = 'block'; this.updateSelectionCounter(); } /** * Exit selection mode */ exitSelectionMode() { document.body.classList.remove('selection-mode'); document.getElementById('exportBtn').style.display = 'none'; document.getElementById('selectionCounter').style.display = 'none'; this.clearSelection(); } /** * Toggle plot selection */ togglePlotSelection(item) { const plotName = this.getPlotName(item); const plotPath = this.getPlotPath(item); if (this.selectedPlots.has(plotName)) { this.selectedPlots.delete(plotName); item.classList.remove('selected'); item.querySelector('.checkbox-icon').textContent = '☐'; } else { if (this.selectedPlots.size >= this.maxPlots) { this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning'); return; } this.selectedPlots.add(plotName); item.classList.add('selected'); item.querySelector('.checkbox-icon').textContent = '☑'; } this.updateSelectionCounter(); } /** * Get plot name from grid item */ getPlotName(item) { const plotName = item.querySelector('.plot-name'); return plotName ? plotName.textContent.trim() : ''; } /** * Get plot PDF path from grid item */ getPlotPath(item) { const link = item.querySelector('a[href$=".pdf"]'); return link ? link.href : ''; } /** * Update selection counter */ updateSelectionCounter() { const counter = document.getElementById('selectionCounter'); if (counter) { counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`; } const exportBtn = document.getElementById('exportBtn'); if (exportBtn) { exportBtn.disabled = this.selectedPlots.size === 0; exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1'; } } /** * Clear all selections */ clearSelection() { this.selectedPlots.clear(); document.querySelectorAll('.grid-item.selected').forEach(item => { item.classList.remove('selected'); const checkbox = item.querySelector('.checkbox-icon'); if (checkbox) checkbox.textContent = '☐'; }); this.updateSelectionCounter(); } /** * Export selected plots to merged PDF */ async exportSelectedPlots() { if (this.selectedPlots.size === 0) { this.showMessage('No plots selected', 'warning'); return; } const plotPaths = Array.from(this.selectedPlots).map(plotName => { const item = Array.from(document.querySelectorAll('.grid-item')) .find(item => this.getPlotName(item) === plotName); return this.getPlotPath(item); }); this.showMessage('Preparing export...', 'info'); try { await this.createMergedPDF(plotPaths); } catch (error) { this.showMessage('Export failed: ' + error.message, 'error'); } } /** * Create merged PDF using Python script */ async createMergedPDF(plotPaths) { // Convert file:// URLs to actual paths const actualPaths = plotPaths.map(url => { if (url.startsWith('file://')) { return url.substring(7); // Remove 'file://' prefix } return url; }); const payload = { plots: actualPaths, layout: this.calculateLayout(actualPaths.length), output_name: `merged_plots_${new Date().toISOString().split('T')[0]}.pdf` }; // Save the request to a JSON file that can be picked up by a Python script const requestData = JSON.stringify(payload, null, 2); // Since we can't directly call Python from the browser, we'll show instructions this.showExportInstructions(requestData); } /** * Calculate optimal layout for given number of plots */ calculateLayout(numPlots) { switch (numPlots) { case 1: return { rows: 1, cols: 1 }; case 2: return { rows: 1, cols: 2 }; case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty case 4: return { rows: 2, cols: 2 }; default: return { rows: 2, cols: 2 }; } } /** * Show export instructions to user */ showExportInstructions(requestData) { const instructions = `

Export Instructions

To export your selected plots, save the following data to a file called export_request.json and run the export script:

Command to run:

cd /work/kschmidt/web && python export_plots.py export_request.json
`; const overlay = document.createElement('div'); overlay.className = 'export-overlay'; overlay.innerHTML = instructions; document.body.appendChild(overlay); } /** * Download the PDF blob */ downloadPDF(blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Show temporary message to user */ showMessage(text, type = 'info') { // Remove existing message const existing = document.querySelector('.export-message'); if (existing) existing.remove(); const message = document.createElement('div'); message.className = `export-message export-message-${type}`; message.textContent = text; document.body.appendChild(message); setTimeout(() => { if (message.parentNode) { message.parentNode.removeChild(message); } }, 3000); } }