/** * Cut Controls for Public Map * Handles cut selection and display on the public map interface */ import { cutManager } from './cut-manager.js'; import { showStatus } from './utils.js'; let cutSelector = null; let cutLegend = null; let cutLegendContent = null; let legendExpanded = false; let mobileOverlayModal = null; let mobileOverlayBtn = null; let cutSelectorListenerAttached = false; // Flag to prevent duplicate listeners let currentDropdownContainer = null; // Track current dropdown container /** * Initialize cut controls */ export async function initializeCutControls() { // Get DOM elements cutSelector = document.getElementById('cut-selector'); cutLegend = document.getElementById('cut-legend'); cutLegendContent = document.getElementById('cut-legend-content'); mobileOverlayModal = document.getElementById('mobile-overlay-modal'); mobileOverlayBtn = document.getElementById('mobile-overlay-btn'); if (!cutSelector) { console.warn('Cut selector not found'); return; } // Debug logging console.log('Mobile overlay button found:', !!mobileOverlayBtn); console.log('Mobile overlay modal found:', !!mobileOverlayModal); // Set up event listeners - remove the change listener since we're not using the select element as a dropdown // cutSelector.addEventListener('change', handleCutSelection); // Set up mobile overlay event delegation if (mobileOverlayModal) { setupMobileOverlayEventListeners(); } // Mobile overlay button is handled in ui-controls.js console.log('Cut controls setup - mobile overlay handled by ui-controls.js'); // Load and populate cuts await loadAndPopulateCuts(); console.log('Cut controls initialized'); } /** * Load cuts and populate selector */ async function loadAndPopulateCuts() { try { const response = await fetch('/api/cuts/public'); if (!response.ok) { console.warn('Failed to fetch cuts:', response.status); // For testing: create mock data if API fails const mockCuts = [ { id: 'test-1', name: 'Test Ward 1', description: 'Test overlay for debugging', category: 'Ward', color: '#ff6b6b', opacity: 0.3, is_public: true, is_official: true, geojson: '{"type":"Polygon","coordinates":[[[-113.5,-113.4],[53.5,53.6],[53.6,53.6],[53.6,53.5],[-113.5,-113.4]]]}' }, { id: 'test-2', name: 'Test Neighborhood', description: 'Another test overlay', category: 'Neighborhood', color: '#4ecdc4', opacity: 0.4, is_public: true, is_official: false } ]; console.log('Using mock cuts for testing'); await processCuts(mockCuts); return; } const data = await response.json(); console.log('Raw API response data:', data); const cuts = data.list || []; console.log('Extracted cuts from API:', cuts); await processCuts(cuts); console.log(`Loaded ${cuts.length} public cuts`); } catch (error) { console.error('Error loading cuts:', error); // Fallback to empty array await processCuts([]); } } /** * Process cuts data (shared logic for real and mock data) */ async function processCuts(cuts) { console.log('Processing cuts:', cuts); console.log('Number of cuts to process:', cuts?.length || 0); // Store cuts globally for reference window.cuts = cuts; console.log('Set window.cuts to:', window.cuts); // Populate both desktop and mobile selectors populateCutSelector(cuts); // Add a small delay to ensure mobile DOM elements are ready setTimeout(() => { populateMobileOverlayOptions(cuts); }, 100); // Auto-display all public cuts await autoDisplayAllPublicCuts(cuts); } /** * Auto-display all public cuts on map load */ async function autoDisplayAllPublicCuts(cuts) { if (!cuts || cuts.length === 0) return; // Filter for public cuts that should auto-display const publicCuts = cuts.filter(cut => { // Handle different possible field names for public visibility const isPublic = cut.is_public || cut.Is_public || cut['Public Visibility'] || cut['Public']; return isPublic === true || isPublic === 1 || isPublic === '1'; }); console.log(`Auto-displaying ${publicCuts.length} public cuts`); // Display all public cuts for (const cut of publicCuts) { try { const normalizedCut = normalizeCutData(cut); // Pass true as second parameter to indicate auto-displayed cutManager.displayCut(normalizedCut, true); } catch (error) { console.error(`Failed to auto-display cut: ${cut.name || cut.Name}`, error); } } // Update UI to show which cuts are active if (publicCuts.length > 0) { updateMultipleCutsUI(); console.log('About to update checkbox states after auto-display...'); // Add a small delay to ensure cuts are fully displayed before updating checkboxes setTimeout(() => { updateCheckboxStates(); // Update checkbox states to reflect auto-displayed cuts updateMobileCheckboxStates(); // Update mobile checkboxes too console.log('Checkbox states updated after auto-display'); }, 100); } } /** * Normalize cut data field names for consistent access */ function normalizeCutData(cut) { return { ...cut, id: cut.id || cut.Id || cut.ID, name: cut.name || cut.Name || 'Unnamed Cut', description: cut.description || cut.Description, color: cut.color || cut.Color || '#3388ff', opacity: cut.opacity || cut.Opacity || 0.3, category: cut.category || cut.Category || 'Other', geojson: cut.geojson || cut.GeoJSON || cut['GeoJSON Data'], is_public: cut.is_public || cut['Public Visibility'], is_official: cut.is_official || cut['Official Cut'] }; } /** * Populate the cut selector with all available cuts */ /** * Populate the cut selector with multi-select checkboxes */ function populateCutSelector(cuts) { if (!cutSelector || !cuts?.length) { console.warn('Cannot populate cut selector - missing selector or cuts'); return; } // Store cuts globally for global functions window.cuts = cuts; // Create the main selector (acts as a button to show/hide dropdown) cutSelector.textContent = 'Manage map overlays...'; // Remove any existing checkbox container const existingContainer = cutSelector.parentNode.querySelector('.cut-checkbox-container'); if (existingContainer) { existingContainer.remove(); } // Create checkbox container const checkboxContainer = document.createElement('div'); checkboxContainer.className = 'cut-checkbox-container'; checkboxContainer.style.display = 'none'; // Explicitly set to none initially // Create header with buttons const header = document.createElement('div'); header.className = 'cut-checkbox-header'; const showAllBtn = document.createElement('button'); showAllBtn.type = 'button'; showAllBtn.className = 'btn btn-sm'; showAllBtn.textContent = 'Show All'; showAllBtn.dataset.action = 'show-all'; const hideAllBtn = document.createElement('button'); hideAllBtn.type = 'button'; hideAllBtn.className = 'btn btn-sm'; hideAllBtn.textContent = 'Hide All'; hideAllBtn.dataset.action = 'hide-all'; header.appendChild(showAllBtn); header.appendChild(hideAllBtn); // Create list container const listContainer = document.createElement('div'); listContainer.className = 'cut-checkbox-list'; // Add checkbox items cuts.forEach(cut => { const normalized = normalizeCutData(cut); const isDisplayed = cutManager.isCutDisplayed(normalized.id); const item = document.createElement('div'); item.className = 'cut-checkbox-item'; item.dataset.cutId = normalized.id; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = isDisplayed; checkbox.dataset.cutId = normalized.id; const colorBox = document.createElement('div'); colorBox.className = 'cut-color-box'; colorBox.style.backgroundColor = normalized.color; const nameSpan = document.createElement('span'); nameSpan.className = 'cut-name'; nameSpan.textContent = normalized.name; item.appendChild(checkbox); item.appendChild(colorBox); item.appendChild(nameSpan); if (normalized.is_official) { const badge = document.createElement('span'); badge.className = 'badge official'; badge.textContent = 'Official'; item.appendChild(badge); } listContainer.appendChild(item); }); checkboxContainer.appendChild(header); checkboxContainer.appendChild(listContainer); // Insert after selector cutSelector.parentNode.appendChild(checkboxContainer); // Setup event listeners using delegation setupCutSelectorEventListeners(checkboxContainer); console.log(`Populated cut selector with ${cuts.length} cuts`); } /** * Setup event listeners for cut selector dropdown */ function setupCutSelectorEventListeners(container) { // Store reference to current container currentDropdownContainer = container; // Handle action buttons and checkbox clicks container.addEventListener('click', async (e) => { e.stopPropagation(); // Prevent event bubbling // Handle action buttons if (e.target.matches('button[data-action]')) { const action = e.target.dataset.action; if (action === 'show-all') { await showAllCuts(); updateCheckboxStates(); updateMobileCheckboxStates(); } else if (action === 'hide-all') { await hideAllCuts(); updateCheckboxStates(); updateMobileCheckboxStates(); } return; } // Handle checkbox clicks if (e.target.matches('input[type="checkbox"]')) { const cutId = e.target.dataset.cutId; console.log('Checkbox clicked for cut ID:', cutId, 'checked:', e.target.checked); console.log('Available window.cuts:', window.cuts); const cut = window.cuts?.find(c => { const normalized = normalizeCutData(c); return String(normalized.id) === String(cutId); }); console.log('Found cut:', cut); if (cut) { const normalized = normalizeCutData(cut); console.log('Normalized cut ID:', normalized.id, 'Original cutId:', cutId); if (e.target.checked) { console.log('Attempting to display cut:', normalized.name); const displayResult = cutManager.displayCut(cut); console.log('Display result:', displayResult); } else { console.log('Attempting to hide cut:', normalized.name, 'with ID:', normalized.id); const hideResult = cutManager.hideCutById(normalized.id); // Use normalized.id instead of cutId console.log('Hide result:', hideResult); // Also try with string conversion in case of ID format mismatch if (!hideResult) { console.log('Trying to hide with string ID:', String(normalized.id)); const hideResult2 = cutManager.hideCutById(String(normalized.id)); console.log('Hide result (string ID):', hideResult2); } } updateMultipleCutsUI(); updateSelectorText(); updateMobileCheckboxStates(); // Update mobile checkboxes too } else { console.warn('Cut not found for ID:', cutId, 'Available cuts:', window.cuts?.length || 'undefined'); console.warn('Window.cuts content:', window.cuts); } return; } // Handle clicking on the item (not checkbox) to toggle if (e.target.closest('.cut-checkbox-item') && !e.target.matches('input')) { const item = e.target.closest('.cut-checkbox-item'); const checkbox = item.querySelector('input[type="checkbox"]'); if (checkbox) { checkbox.checked = !checkbox.checked; // Trigger the checkbox change event const clickEvent = new Event('click', { bubbles: true }); checkbox.dispatchEvent(clickEvent); } } }); // Only attach cutSelector event listener once if (!cutSelectorListenerAttached) { cutSelector.addEventListener('click', (e) => { // Use current active container const activeContainer = currentDropdownContainer; if (!activeContainer) return; // Always toggle based on current display state if (activeContainer.style.display === 'block') { activeContainer.style.display = 'none'; } else { activeContainer.style.display = 'block'; // Update checkbox states when opening dropdown to reflect current state updateCheckboxStates(); updateSelectorText(); } }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { const activeContainer = currentDropdownContainer; if (activeContainer && !cutSelector.contains(e.target) && !activeContainer.contains(e.target)) { activeContainer.style.display = 'none'; } }); cutSelectorListenerAttached = true; } } /** * Update checkbox states to match current displayed cuts */ function updateCheckboxStates() { const checkboxes = document.querySelectorAll('.cut-checkbox-item input[type="checkbox"]'); checkboxes.forEach(cb => { const cutId = cb.dataset.cutId; const isDisplayed = cutManager.isCutDisplayed(cutId); cb.checked = isDisplayed; }); } /** * Update mobile checkbox states to match current displayed cuts */ function updateMobileCheckboxStates() { document.querySelectorAll('input[name="mobile-cut"]').forEach(cb => { const cutId = cb.dataset.cutId; cb.checked = cutManager.isCutDisplayed(cutId); }); } /** * Update selector text based on active cuts */ function updateSelectorText() { const activeCuts = cutManager.getDisplayedCuts(); if (activeCuts.length === 0) { cutSelector.textContent = 'Select map overlays...'; } else if (activeCuts.length === 1) { cutSelector.textContent = `📍 ${activeCuts[0].name}`; } else { cutSelector.textContent = `📍 ${activeCuts.length} overlays active`; } } /** * Show all cuts function */ async function showAllCuts() { if (window.cuts) { for (const cut of window.cuts) { const normalized = normalizeCutData(cut); if (!cutManager.isCutDisplayed(normalized.id)) { cutManager.displayCut(cut); console.log(`Displayed cut: ${normalized.name}`); } } updateMultipleCutsUI(); updateSelectorText(); console.log('All cuts shown'); } } /** * Hide all cuts function (updated) */ async function hideAllCuts() { cutManager.hideAllCuts(); updateMultipleCutsUI(); updateSelectorText(); console.log('All cuts hidden by user'); } /** * Handle cut selection change */ async function handleCutSelection(event) { const selectedValue = event.target.value; if (!selectedValue) { // Reset selector to show current state refreshSelectorUI(); return; } if (selectedValue === '__show_all__') { // Show all cuts const allCuts = window.cuts || []; for (const cut of allCuts) { const normalizedCut = normalizeCutData(cut); if (!cutManager.isCutDisplayed(normalizedCut.id)) { cutManager.displayCut(normalizedCut); console.log(`Displayed cut: ${normalizedCut.name}`); } } refreshSelectorUI(); updateLegendAndUI(); return; } if (selectedValue === '__hide_all__') { // Hide all cuts hideAllCuts(); return; } // Individual cut selection - toggle the cut const cut = window.cuts?.find(c => { const cutId = c.id || c.Id || c.ID; return cutId == selectedValue; }); if (cut) { const normalizedCut = normalizeCutData(cut); if (cutManager.isCutDisplayed(normalizedCut.id)) { cutManager.hideCutById(normalizedCut.id); console.log(`Hidden cut: ${normalizedCut.name}`); } else { cutManager.displayCut(normalizedCut); console.log(`Displayed cut: ${normalizedCut.name}`); } // Refresh the selector to show updated state refreshSelectorUI(); updateLegendAndUI(); } } /** * Refresh the selector UI to show current state */ function refreshSelectorUI() { // Re-populate the selector to update the icons if (window.cuts && window.cuts.length > 0) { populateCutSelector(window.cuts); } // Update the selector value based on displayed cuts const displayedCuts = cutManager.getDisplayedCuts(); if (displayedCuts.length === 0) { // No cuts displayed - reset to placeholder cutSelector.value = ''; } else if (displayedCuts.length === 1) { // Single cut displayed cutSelector.value = displayedCuts[0].id; } else { // Multiple cuts displayed - show count in placeholder cutSelector.value = ''; setTimeout(() => { const firstOption = cutSelector.querySelector('option[value=""]'); if (firstOption) { firstOption.textContent = `${displayedCuts.length} cuts active - Select actions...`; } }, 0); } } /** * Update legend and UI components */ function updateLegendAndUI() { const activeCuts = cutManager.getDisplayedCuts(); if (activeCuts.length > 0) { showMultipleCutsLegend(activeCuts); updateCutControlsUI(activeCuts); console.log(`Active cuts: ${activeCuts.map(c => c.name || 'Unnamed').join(', ')}`); } else { hideCutLegend(); updateCutControlsUI([]); console.log('No cuts currently displayed'); } } /** * Show cut legend with cut information */ function showCutLegend(cutData) { if (!cutLegend) return; // Update legend content const legendColor = document.getElementById('legend-color'); const legendName = document.getElementById('legend-name'); const legendDescription = document.getElementById('legend-description'); // Handle different possible field names const color = cutData.color || cutData.Color || '#3388ff'; const opacity = cutData.opacity || cutData.Opacity || 0.3; const name = cutData.name || cutData.Name || 'Unnamed Cut'; const description = cutData.description || cutData.Description || ''; if (legendColor) { legendColor.style.backgroundColor = color; legendColor.style.opacity = opacity; } if (legendName) { legendName.textContent = name; } if (legendDescription) { legendDescription.textContent = description; legendDescription.style.display = description ? 'block' : 'none'; } // Show legend cutLegend.classList.add('visible'); // Auto-expand legend content for first-time users if (!legendExpanded) { expandLegend(); } } /** * Hide cut legend */ function hideCutLegend() { if (!cutLegend) return; cutLegend.classList.remove('visible'); collapseLegend(); } /** * Toggle cut legend expansion */ export function toggleCutLegend() { if (legendExpanded) { collapseLegend(); } else { expandLegend(); } } /** * Expand legend content */ function expandLegend() { if (!cutLegendContent) return; cutLegendContent.classList.add('expanded'); legendExpanded = true; // Update toggle indicator const toggle = cutLegend.querySelector('.legend-toggle'); if (toggle) { toggle.textContent = '▲'; } } /** * Collapse legend content */ function collapseLegend() { if (!cutLegendContent) return; cutLegendContent.classList.remove('expanded'); legendExpanded = false; // Update toggle indicator const toggle = cutLegend.querySelector('.legend-toggle'); if (toggle) { toggle.textContent = '▼'; } } /** * Refresh cut controls (e.g., when new cuts are added) */ export async function refreshCutControls() { await loadAndPopulateCuts(); } /** * Get currently selected cut ID */ export function getCurrentCutSelection() { return cutSelector ? cutSelector.value : null; } /** * Set cut selection programmatically */ export function setCutSelection(cutId) { if (!cutSelector) return false; cutSelector.value = cutId; cutSelector.dispatchEvent(new Event('change')); // Also update mobile selection const mobileRadio = document.querySelector(`input[name="mobile-overlay"][value="${cutId || ''}"]`); if (mobileRadio) { mobileRadio.checked = true; updateMobileOverlayInfo(cutId); } return true; } /** * Populate mobile overlay options with multi-select support */ function populateMobileOverlayOptions(cuts) { console.log('populateMobileOverlayOptions called with', cuts?.length, 'cuts'); const container = document.getElementById('mobile-overlay-list'); // Changed from mobile-overlay-options if (!container) { console.warn('Mobile overlay list container not found'); return; } console.log('Mobile overlay list container found'); container.innerHTML = ''; if (!cuts || cuts.length === 0) { container.innerHTML = '