import { map } from './map-manager.js'; import { showStatus } from './utils.js'; import { openAddModal } from './location-manager.js'; let edmontonParcelsLayer = null; let isLoading = false; let isLayerVisible = false; let lastBounds = null; let lastZoom = null; /** * Toggles the visibility of the Edmonton Parcel Addresses layer. * Uses smart loading based on map bounds and zoom level. */ export async function toggleEdmontonParcelsLayer() { const toggleBtn = document.getElementById('toggle-edmonton-layer-btn'); const mobileToggleBtn = document.getElementById('mobile-toggle-edmonton-layer-btn'); if (isLoading) { console.log('Edmonton layer is already loading...'); return; } if (isLayerVisible) { // Hide the layer if (edmontonParcelsLayer && map.hasLayer(edmontonParcelsLayer)) { map.removeLayer(edmontonParcelsLayer); } isLayerVisible = false; toggleBtn?.classList.remove('active'); mobileToggleBtn?.classList.remove('active'); console.log('Edmonton addresses layer hidden'); // Remove map event listeners map.off('moveend', onMapMoveEnd); map.off('zoomend', onMapZoomEnd); } else { // Show the layer isLayerVisible = true; toggleBtn?.classList.add('active'); mobileToggleBtn?.classList.add('active'); console.log('Edmonton addresses layer enabled'); // Add map event listeners for dynamic loading map.on('moveend', onMapMoveEnd); map.on('zoomend', onMapZoomEnd); // Load data for current view await loadEdmontonData(); } } /** * Loads Edmonton data for the current map bounds and zoom level */ async function loadEdmontonData(force = false) { if (!isLayerVisible || isLoading) return; const currentBounds = map.getBounds(); const currentZoom = map.getZoom(); // Check if we need to reload data if (!force && lastBounds && lastZoom === currentZoom && lastBounds.contains(currentBounds)) { return; // Current view is already covered } // Only load data if zoomed in enough (reduces server load) if (currentZoom < 11) { if (edmontonParcelsLayer && map.hasLayer(edmontonParcelsLayer)) { map.removeLayer(edmontonParcelsLayer); } console.log('Zoom level too low for Edmonton data, zoom in to see addresses'); return; } isLoading = true; const toggleBtn = document.getElementById('toggle-edmonton-layer-btn'); const mobileToggleBtn = document.getElementById('mobile-toggle-edmonton-layer-btn'); if (toggleBtn) toggleBtn.disabled = true; if (mobileToggleBtn) mobileToggleBtn.disabled = true; try { // Expand bounds slightly to avoid edge loading issues const expandedBounds = currentBounds.pad(0.1); const boundsString = [ expandedBounds.getSouth(), expandedBounds.getWest(), expandedBounds.getNorth(), expandedBounds.getEast() ].join(','); const params = new URLSearchParams({ bounds: boundsString, zoom: currentZoom.toString(), limit: currentZoom > 15 ? '2000' : currentZoom > 12 ? '1000' : '500' }); console.log(`Loading Edmonton addresses for zoom level ${currentZoom}, bounds: ${boundsString}`); const response = await fetch(`/api/external/edmonton-parcels?${params}`); if (!response.ok) { throw new Error('Failed to fetch layer data from server.'); } const result = await response.json(); if (result.success) { // Remove existing layer if (edmontonParcelsLayer && map.hasLayer(edmontonParcelsLayer)) { map.removeLayer(edmontonParcelsLayer); } // Create new layer with clustering optimized for the zoom level const clusterRadius = currentZoom > 15 ? 30 : currentZoom > 12 ? 50 : 80; edmontonParcelsLayer = L.markerClusterGroup({ chunkedLoading: true, maxClusterRadius: clusterRadius, disableClusteringAtZoom: 18, showCoverageOnHover: false, zoomToBoundsOnClick: true, spiderfyOnMaxZoom: true, removeOutsideVisibleBounds: true }); const geoJsonLayer = L.geoJSON(result.data, { pointToLayer: (feature, latlng) => { const isMultiUnit = feature.properties.isMultiUnit; if (isMultiUnit) { // Use a custom HTML marker for multi-unit buildings (square) const size = Math.max(12, Math.min(24, (currentZoom - 10) * 2)); const icon = L.divIcon({ className: 'multi-unit-marker', html: `
`, iconSize: [size, size], iconAnchor: [size/2, size/2], popupAnchor: [0, -size/2] }); return L.marker(latlng, { icon: icon, zIndexOffset: -100 // Behind NocoDB locations }); } else { // Use circle marker for single units (round, red) return L.circleMarker(latlng, { radius: Math.max(3, Math.min(6, currentZoom - 10)), fillColor: "#ba001e", color: "#ffffff", weight: 1, opacity: 0.9, fillOpacity: 0.7, zIndexOffset: -100 // Behind NocoDB locations }); } }, onEachFeature: (feature, layer) => { const props = feature.properties; if (props.isMultiUnit) { // Create apartment-style popup for multi-unit buildings const popup = L.popup({ maxWidth: 320, minWidth: 280, closeButton: true, className: 'apartment-popup' }).setContent(createApartmentPopup(props)); layer.bindPopup(popup); // Add event listener for when popup opens layer.on('popupopen', function(e) { setupApartmentPopupListeners(props); }); } else { // Simple popup for single units const suite = props.suites && props.suites[0] ? props.suites[0] : {}; // Better suite/unit display logic let suiteDisplay = ''; if (suite.suite && suite.suite.trim()) { const unitLabel = (suite.object_type && suite.object_type.toLowerCase().includes('suite')) ? 'Suite' : 'Unit'; suiteDisplay = `${unitLabel}: ${suite.suite.trim()}
`; } const popupContent = `
🏠 ${props.address}
${suiteDisplay} ${props.neighborhood ? `📍 ${props.neighborhood}
` : ''} ${suite.object_type ? `${suite.object_type}
` : ''} City of Edmonton Data
`; layer.bindPopup(popupContent, { maxWidth: 200, closeButton: true }); // Add event listener for popup open to attach button listeners layer.on('popupopen', function(e) { setupSingleUnitPopupListeners(); }); } } }); edmontonParcelsLayer.addLayer(geoJsonLayer); map.addLayer(edmontonParcelsLayer); // Update tracking variables lastBounds = expandedBounds; lastZoom = currentZoom; const statusMessage = result.data.metadata?.hasMore ? `Loaded ${result.data.features.length} addresses (more available at higher zoom)` : `Loaded ${result.data.features.length} Edmonton addresses`; console.log(statusMessage); } else { throw new Error(result.error || 'Could not load layer data.'); } } catch (error) { console.error('Error loading Edmonton parcels layer:', error); // Only show error status, not success messages showStatus(error.message, 'error'); } finally { isLoading = false; if (toggleBtn) toggleBtn.disabled = false; if (mobileToggleBtn) mobileToggleBtn.disabled = false; } } /** * Handle map move events */ function onMapMoveEnd() { // Debounce the loading to avoid too many requests clearTimeout(onMapMoveEnd.timeout); onMapMoveEnd.timeout = setTimeout(() => { loadEdmontonData(); }, 500); } /** * Handle map zoom events */ function onMapZoomEnd() { // Force reload on zoom change loadEdmontonData(true); } /** * Creates an apartment-style popup for multi-unit buildings */ function createApartmentPopup(props) { const suites = props.suites || []; const totalSuites = suites.length; const popupId = `apartment-popup-${Math.random().toString(36).substr(2, 9)}`; // Truncate address if too long for mobile const displayAddress = props.address.length > 30 ? props.address.substring(0, 30) + '...' : props.address; const popupContent = `
🏢 ${displayAddress}
${totalSuites} units • ${props.neighborhood || 'Edmonton'}
Apt
${suites[0].suite ? `Suite ${suites[0].suite.trim()}` : `Unit 1`}
${suites[0].object_type || 'Residential Unit'}
${suites[0].house_number && suites[0].street_name ? `
📍 ${suites[0].house_number} ${suites[0].street_name}
` : ''} ${suites[0].record_id ? `
ID: ${suites[0].record_id}
` : ''}
City of Edmonton Open Data
`; return popupContent; } /** * Sets up event listeners for apartment popup suite selection */ function setupApartmentPopupListeners(props) { const suites = props.suites || []; // Find the popup container in the DOM const container = document.querySelector('.apartment-building-popup'); if (!container) { console.log('Apartment popup container not found'); return; } const suiteSelector = container.querySelector('.suite-selector'); const suiteDetails = container.querySelector('.suite-details'); if (!suiteSelector || !suiteDetails) { console.log('Apartment popup elements not found'); return; } function updateSuiteDisplay(selectedIndex) { const suite = suites[selectedIndex]; // Better logic for displaying unit/suite information let unitLabel = 'Unit'; let unitNumber = suite.suite ? suite.suite.trim() : ''; // If no suite number but has object type that suggests it's a suite if (!unitNumber && suite.object_type && suite.object_type.toLowerCase().includes('suite')) { unitNumber = `${selectedIndex + 1}`; } // If still no unit number, use sequential numbering if (!unitNumber) { unitNumber = `${selectedIndex + 1}`; } // Determine if it's a suite or unit based on the data if (suite.object_type && suite.object_type.toLowerCase().includes('suite')) { unitLabel = 'Suite'; } suiteDetails.innerHTML = `
${unitLabel} ${unitNumber}
${suite.object_type || 'Residential Unit'}
${suite.house_number && suite.street_name ? `
📍 ${suite.house_number} ${suite.street_name}
` : ''} ${suite.record_id ? `
ID: ${suite.record_id}
` : ''} `; } // Set up suite selector dropdown event listener suiteSelector.addEventListener('change', (e) => { e.stopPropagation(); const selectedIndex = parseInt(e.target.value); console.log('Suite selected:', selectedIndex); updateSuiteDisplay(selectedIndex); }); // Initialize with first suite selected updateSuiteDisplay(0); console.log(`Apartment popup suite selector set up for ${suites.length} suites`); // Set up the "Add to Database" button listener for apartments const addBtn = container.querySelector('.add-apartment-to-database-btn'); if (addBtn) { addBtn.addEventListener('click', (e) => { e.stopPropagation(); const address = addBtn.getAttribute('data-address'); const lat = parseFloat(addBtn.getAttribute('data-lat')); const lng = parseFloat(addBtn.getAttribute('data-lng')); const neighborhood = addBtn.getAttribute('data-neighborhood'); const suitesData = JSON.parse(addBtn.getAttribute('data-suites') || '[]'); // Get the currently selected suite from dropdown const selectedSuiteIndex = parseInt(suiteSelector.value); const selectedSuite = suitesData[selectedSuiteIndex] || suitesData[0]; // Close the popup first map.closePopup(); // Open the add modal but prevent the automatic address lookup openAddModal(lat, lng, false); // Pre-fill the form with Edmonton data, focusing on the selected suite setTimeout(() => { prefillAddForm({ address: address, neighborhood: neighborhood, lat: lat, lng: lng, suite: selectedSuite.suite || '', objectType: selectedSuite.object_type || '', notes: `Selected unit from multi-unit building (${suitesData.length} total units). Imported from City of Edmonton data.`, isMultiUnit: true, suites: suitesData, selectedSuite: selectedSuite }); }, 100); }); } } /** * Sets up event listeners for single unit popup "Add to Database" button */ function setupSingleUnitPopupListeners() { // Wait for popup to be rendered in DOM setTimeout(() => { const addBtn = document.querySelector('.add-to-database-btn'); if (addBtn) { addBtn.addEventListener('click', (e) => { e.stopPropagation(); const address = addBtn.getAttribute('data-address'); const lat = parseFloat(addBtn.getAttribute('data-lat')); const lng = parseFloat(addBtn.getAttribute('data-lng')); const neighborhood = addBtn.getAttribute('data-neighborhood'); const suite = addBtn.getAttribute('data-suite'); const objectType = addBtn.getAttribute('data-object-type'); // Close the popup first map.closePopup(); // Open the add modal but prevent the automatic address lookup openAddModal(lat, lng, false); // Pre-fill the form with Edmonton data setTimeout(() => { prefillAddForm({ address: address, neighborhood: neighborhood, lat: lat, lng: lng, suite: suite, objectType: objectType, notes: `Imported from City of Edmonton data.${objectType ? ` (${objectType})` : ''}`, isMultiUnit: false }); }, 100); }); } }, 50); } /** * Pre-fills the add location form with city data */ async function prefillAddForm(data) { try { // Import UI controls dynamically to avoid circular dependencies const { resetAddressConfirmation } = await import('./ui-controls.js'); // First, reset any existing state resetAddressConfirmation('add'); // Basic form fields if (data.address) { const addressField = document.getElementById('location-address'); if (addressField) { addressField.value = data.address; } } // Put suite number in the unit field if (data.suite && data.suite.trim()) { const unitField = document.getElementById('location-unit'); if (unitField) { unitField.value = data.suite.trim(); } } // Notes field with additional context const notesField = document.getElementById('location-notes'); if (notesField) { let notes = data.notes || ''; // Add neighborhood information if available if (data.neighborhood && data.neighborhood.trim()) { notes += `\nNeighborhood: ${data.neighborhood}`; } // Add object type if available if (data.objectType && data.objectType.trim()) { notes += `\nUnit Type: ${data.objectType}`; } // For multi-unit buildings, add information about the selected suite if (data.isMultiUnit && data.selectedSuite) { notes += `\nSelected Suite: ${data.selectedSuite.suite || 'N/A'}`; notes += `\nSuite Type: ${data.selectedSuite.object_type || 'N/A'}`; } // For single units, add basic suite information else if (data.suite && data.suite.trim()) { notes += `\nSuite: ${data.suite}`; } notesField.value = notes.trim(); } // Set coordinates (already set by openAddModal, but ensure they're correct) const latField = document.getElementById('location-lat'); const lngField = document.getElementById('location-lng'); const geoField = document.getElementById('geo-location'); if (latField && data.lat) { latField.value = data.lat.toFixed(8); } if (lngField && data.lng) { lngField.value = data.lng.toFixed(8); } if (geoField && data.lat && data.lng) { geoField.value = `${data.lat.toFixed(8)};${data.lng.toFixed(8)}`; } // Mark the address as confirmed since it's coming from a trusted source const { setAddressConfirmed } = await import('./ui-controls.js'); setAddressConfirmed('add', true); console.log('Form pre-filled and confirmed with city data:', data); } catch (error) { console.error('Error pre-filling form:', error); showStatus('Error pre-filling form with city data', 'error'); } }