// TreeTrack Enhanced Map with Authentication and Tree Management class TreeTrackMap { constructor() { this.map = null; this.tempMarker = null; this.selectedLocation = null; this.treeMarkers = []; this.markerClusterGroup = null; this.userLocation = null; this.isLocationSelected = false; // Authentication properties this.currentUser = null; this.authToken = null; this.init(); } async init() { console.log('TreeTrackMap initialization started'); // Check authentication first console.log('Checking authentication...'); if (!await this.checkAuthentication()) { console.log('Authentication failed, redirecting to login'); window.location.href = '/login'; return; } console.log('Authentication successful'); this.showLoading(); try { console.log('Initializing map...'); await this.initializeMap(); console.log('Setting up event listeners...'); this.setupEventListeners(); console.log('Setting up user interface...'); this.setupUserInterface(); // Load trees asynchronously without blocking UI console.log('Starting tree loading (async)...'); this.loadTrees().catch(error => { console.error('Tree loading failed:', error); this.showMessage('Failed to load trees', 'error'); }); setTimeout(() => { this.hideLoading(); this.showGestureHint(); console.log('Map initialization complete!'); }, 500); } catch (error) { console.error('Map initialization failed:', error); console.error('Error stack:', error.stack); this.showMessage('Failed to initialize map. Please refresh the page.', 'error'); this.hideLoading(); } } // Authentication methods async checkAuthentication() { console.log('Checking for auth token...'); const token = localStorage.getItem('auth_token'); if (!token) { console.log('No auth token found in localStorage'); return false; } console.log('Auth token found, validating...'); try { const response = await fetch('/api/auth/validate', { headers: { 'Authorization': `Bearer ${token}` } }); console.log('Auth validation response status:', response.status); if (response.ok) { const data = await response.json(); this.currentUser = data.user; this.authToken = token; console.log('Authentication successful for user:', data.user.username); console.log('User permissions:', data.user.permissions); return true; } else { console.log('Token invalid, status:', response.status); // Token invalid, remove it localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); return false; } } catch (error) { console.error('Auth validation error:', error); return false; } } setupUserInterface() { // Add user info to header this.displayUserInfo(); // Add logout functionality this.addLogoutButton(); // Show welcome button only for demo users this.setupWelcomeButton(); // Setup instructions panel this.setupInstructionsPanel(); } displayUserInfo() { if (!this.currentUser) return; // Update existing user info elements in the HTML const userAvatar = document.getElementById('userAvatar'); const userName = document.getElementById('userName'); const userRole = document.getElementById('userRole'); if (userAvatar && this.currentUser.full_name) { userAvatar.textContent = this.currentUser.full_name.charAt(0).toUpperCase(); } if (userName) { userName.textContent = this.currentUser.full_name || this.currentUser.username || 'User'; } if (userRole) { userRole.textContent = this.currentUser.role || 'User'; } } addLogoutButton() { // Use existing logout button instead of creating a new one const existingLogoutBtn = document.getElementById('logoutBtn'); if (existingLogoutBtn) { existingLogoutBtn.addEventListener('click', () => this.logout()); } } isDemoUser() { if (!this.currentUser) return false; return this.currentUser.role === 'demo_user' || this.currentUser.username === 'demo_user' || (this.currentUser.permissions && this.currentUser.permissions.includes('demo_view')); } setupWelcomeButton() { const welcomeBtn = document.getElementById('welcomeBtn'); if (welcomeBtn && this.isDemoUser()) { welcomeBtn.style.display = 'inline-flex'; } } setupInstructionsPanel() { // Always show instructions panel (persistent like map controls) const instructionsPanel = document.getElementById('instructionsPanel'); const toggleBtn = document.getElementById('instructionsToggle'); if (instructionsPanel) { // Keep instructions visible (remove hidden class if present) instructionsPanel.classList.remove('hidden'); } if (toggleBtn) { // Hide toggle button initially since panel is visible toggleBtn.classList.add('hidden'); } // Setup close/toggle functionality for manual control const closeBtn = document.getElementById('closeInstructions'); if (closeBtn) { closeBtn.addEventListener('click', () => { this.toggleInstructions(); }); } // Setup toggle button functionality if (toggleBtn) { toggleBtn.addEventListener('click', () => { this.toggleInstructions(); }); } } toggleInstructions() { const instructionsPanel = document.getElementById('instructionsPanel'); const toggleBtn = document.getElementById('instructionsToggle'); if (instructionsPanel) { instructionsPanel.classList.toggle('hidden'); // Show/hide toggle button based on panel visibility if (toggleBtn) { if (instructionsPanel.classList.contains('hidden')) { // Panel is hidden, show toggle button toggleBtn.classList.remove('hidden'); } else { // Panel is visible, hide toggle button toggleBtn.classList.add('hidden'); } } } } hideInstructions() { const instructionsPanel = document.getElementById('instructionsPanel'); if (instructionsPanel) { instructionsPanel.classList.add('hidden'); } } showInstructions() { const instructionsPanel = document.getElementById('instructionsPanel'); if (instructionsPanel) { instructionsPanel.classList.remove('hidden'); } } async logout() { try { await fetch('/api/auth/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${this.authToken}` } }); } catch (error) { console.error('Logout error:', error); } finally { localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; } } // Enhanced API calls with authentication async authenticatedFetch(url, options = {}) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.authToken}`, ...options.headers }; const response = await fetch(url, { ...options, headers }); if (response.status === 401) { // Token expired or invalid localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; return null; } return response; } // Permission checking methods canEditTree(createdBy) { if (!this.currentUser) return false; // Admin and system can edit any tree if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) { return true; } // Users can edit trees they created if (this.currentUser.permissions.includes('edit_own') && createdBy === this.currentUser.username) { return true; } // Users with delete permission can edit any tree if (this.currentUser.permissions.includes('delete')) { return true; } return false; } canDeleteTree(createdBy) { if (!this.currentUser) return false; // Only admin and system can delete trees if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) { return true; } // Users with explicit delete permission if (this.currentUser.permissions.includes('delete')) { return true; } return false; } async initializeMap() { console.log('Initializing map...'); // Center on Tezpur, Assam - the actual research area const tezpurLocation = [26.6340, 92.7840]; // Tezpur coordinates // Initialize map with clustering-optimized zoom level this.map = L.map('map', { center: tezpurLocation, zoom: 15, // Start with good clustering view (matches center view) maxZoom: 17, // Allow enough zoom for spiderfy to work properly minZoom: 10, // Set reasonable minimum zoom zoomControl: true, attributionControl: true, preferCanvas: true // Better performance for many markers }); // Add multiple beautiful tile layers with spiderfy-compatible zoom limits const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri — Source: Esri, Maxar, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, AeroGRID, IGN, and the GIS User Community', maxZoom: 17, // Allow higher zoom for spiderfy functionality minZoom: 10 }); const terrainLayer = L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png', { attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', maxZoom: 17, minZoom: 10 }); const streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 17, minZoom: 10 }); // Use satellite as primary layer for Tezpur's natural beauty satelliteLayer.addTo(this.map); // Add layer control for users to switch const baseMaps = { "Satellite": satelliteLayer, "Terrain": terrainLayer, "Street Map": streetLayer }; L.control.layers(baseMaps).addTo(this.map); // Initialize marker clustering with clean spiderfy behavior (no blue polygons) this.markerClusterGroup = L.markerClusterGroup({ chunkedLoading: true, chunkInterval: 100, // Faster processing for single query chunkDelay: 25, // Reduced delay maxClusterRadius: 60, // Balanced radius for good clustering and spiderfy spiderfyOnMaxZoom: true, // Enable spiral/flower view on max zoom spiderfyDistanceMultiplier: 1.5, // Larger spiral spread showCoverageOnHover: false, // DISABLE blue polygon coverage - it's hideous! zoomToBoundsOnClick: false, // Disable auto-zoom, prefer spiderfy removeOutsideVisibleBounds: true, // Performance optimization animate: false, // Disable animations to prevent ghost trees animateAddingMarkers: false, // Disable marker addition animations disableClusteringAtZoom: 18, // Allow clustering until very high zoom // Improved spiderfy settings for smoother spiral view without ghosts spiderfyShapePositions: function(count, centerPt) { const positions = []; const angleStep = (2 * Math.PI) / count; const baseRadius = 45; // Base distance from center for (let i = 0; i < count; i++) { const angle = i * angleStep; const spiralRadius = baseRadius + (i * 3); // Tighter spiral to reduce ghosting positions.push({ x: centerPt.x + spiralRadius * Math.cos(angle), y: centerPt.y + spiralRadius * Math.sin(angle) }); } return positions; } }); this.map.addLayer(this.markerClusterGroup); // Map click handler for pin dropping this.map.on('click', (e) => { this.onMapClick(e); }); console.log('Map initialized successfully with clustering'); } setupEventListeners() { console.log('Setting up event listeners...'); // My Location button document.getElementById('myLocationBtn').addEventListener('click', () => { this.getCurrentLocation(); }); // Clear Pins button document.getElementById('clearPinsBtn').addEventListener('click', () => { this.clearTempMarker(); }); // Center Map button const centerMapBtn = document.getElementById('centerMapBtn'); if (centerMapBtn) { centerMapBtn.addEventListener('click', () => { this.centerMapToTrees(); }); } // Use Location button document.getElementById('useLocationBtn').addEventListener('click', () => { this.useSelectedLocation(); }); // Cancel button document.getElementById('cancelBtn').addEventListener('click', () => { this.cancelLocationSelection(); }); console.log('Event listeners setup complete'); } onMapClick(e) { console.log('Map clicked at:', e.latlng); // Remove existing temp marker first (before setting new location) if (this.tempMarker) { this.map.removeLayer(this.tempMarker); this.tempMarker = null; } // Now set the new location this.selectedLocation = e.latlng; this.isLocationSelected = true; // Create beautiful tree-shaped temp marker with red coloring for selection const tempColors = { canopy1: '#ef4444', // Red for temp marker canopy2: '#dc2626', canopy3: '#b91c1c', trunk: '#7f5a44', // Keep trunk natural shadow: 'rgba(185, 28, 28, 0.4)' }; const tempIcon = L.divIcon({ html: `
`, className: 'custom-marker-icon tree-pin-temp realistic-tree-temp', iconSize: [36, 44], iconAnchor: [18, 42], popupAnchor: [0, -44] }); this.tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: tempIcon }).addTo(this.map); // Update coordinates display document.getElementById('latValue').textContent = e.latlng.lat.toFixed(6); document.getElementById('lngValue').textContent = e.latlng.lng.toFixed(6); // Show info panel this.showInfoPanel(); } clearTempMarker() { if (this.tempMarker) { this.map.removeLayer(this.tempMarker); this.tempMarker = null; } this.selectedLocation = null; this.isLocationSelected = false; this.hideInfoPanel(); } showInfoPanel() { const panel = document.getElementById('locationPanel'); if (panel) { panel.classList.add('active'); } } hideInfoPanel() { const panel = document.getElementById('locationPanel'); if (panel) { panel.classList.remove('active'); } } useSelectedLocation() { if (!this.selectedLocation || !this.isLocationSelected) { this.showMessage('No location selected. Please click on the map to drop a pin first.', 'error'); return; } try { // Store location for the form page const locationData = { lat: this.selectedLocation.lat, lng: this.selectedLocation.lng }; localStorage.setItem('selectedLocation', JSON.stringify(locationData)); // Clear any previous messages and show success const messageElement = document.getElementById('message'); if(messageElement) messageElement.classList.remove('show'); this.showMessage('Location saved! Redirecting to form...', 'success'); // Redirect after a short delay setTimeout(() => { window.location.href = '/'; }, 1500); } catch (error) { console.error('Error saving location:', error); this.showMessage('Error saving location. Please try again.', 'error'); } } cancelLocationSelection() { this.clearTempMarker(); this.showMessage('Selection cancelled', 'info'); } centerMapToTrees() { if (this.treeMarkers.length === 0) { this.showMessage('No trees to center on', 'info'); return; } // Fit map to show all trees with clustering view (like screenshot) const group = new L.featureGroup(this.treeMarkers); this.map.fitBounds(group.getBounds().pad(0.1), { maxZoom: 15 }); this.showMessage('Map centered on all trees', 'success'); } getCurrentLocation() { console.log('Getting current location...'); if (!navigator.geolocation) { this.showMessage('Geolocation not supported by this browser', 'error'); return; } const myLocationBtn = document.getElementById('myLocationBtn'); myLocationBtn.textContent = 'Getting...'; myLocationBtn.disabled = true; navigator.geolocation.getCurrentPosition( (position) => { console.log('Location found:', position.coords); const lat = position.coords.latitude; const lng = position.coords.longitude; this.userLocation = { lat, lng }; // Center map on user location this.map.setView([lat, lng], 16); // Add user location marker if (this.userLocationMarker) { this.map.removeLayer(this.userLocationMarker); } const userIcon = L.divIcon({ html: `
`, className: 'custom-marker-icon', iconSize: [24, 32], iconAnchor: [12, 32], popupAnchor: [0, -32] }); this.userLocationMarker = L.marker([lat, lng], { icon: userIcon }).addTo(this.map); // Add tooltip this.userLocationMarker.bindTooltip('Your Location', { permanent: false, direction: 'top', offset: [0, -10], className: 'tree-tooltip' }); this.showMessage('Location found successfully', 'success'); myLocationBtn.textContent = 'My Location'; myLocationBtn.disabled = false; }, (error) => { console.error('Geolocation error:', error); let errorMessage = 'Failed to get location'; switch (error.code) { case error.PERMISSION_DENIED: errorMessage = 'Location access denied by user'; break; case error.POSITION_UNAVAILABLE: errorMessage = 'Location information unavailable'; break; case error.TIMEOUT: errorMessage = 'Location request timed out'; break; } this.showMessage(errorMessage, 'error'); myLocationBtn.textContent = 'My Location'; myLocationBtn.disabled = false; }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 } ); } async loadTrees() { console.log('🗺️ Starting tree loading process (RLS-enabled backend)...'); try { // Clear existing tree markers console.log('Clearing existing tree markers...'); this.clearTreeMarkers(); // Check if we have auth token if (!this.authToken) { console.error('No auth token available for API calls'); this.showMessage('Authentication required to load trees', 'error'); return; } // Check for cached tree data first const cacheKey = `treetrack_trees_${this.currentUser.username}`; const cachedData = localStorage.getItem(cacheKey); const cacheTimestamp = localStorage.getItem(`${cacheKey}_timestamp`); // Use cached data if it's less than 30 minutes old const cacheAge = Date.now() - parseInt(cacheTimestamp || '0'); const maxCacheAge = 30 * 60 * 1000; // 30 minutes if (cachedData && cacheAge < maxCacheAge) { console.log('Using cached tree data (age: ' + Math.round(cacheAge / 1000) + 's)'); const allTrees = JSON.parse(cachedData); this.renderTreesToMap(allTrees); this.showMessage(`Loaded ${allTrees.length} trees from cache`, 'success'); return; } console.log('Cache miss or expired, loading fresh data...'); // Load trees in batches let allTrees = []; let offset = 0; const batchSize = 1000; // API batch size (Supabase limit) let hasMoreTrees = true; console.log('Starting batch loading process...'); const treeCountEl = document.getElementById('treeCount'); if (treeCountEl) { treeCountEl.textContent = 'Loading trees...'; } else { console.warn('treeCount element not found'); } while (hasMoreTrees && allTrees.length < 3000) { // Safety limit console.log(`Loading batch: offset=${offset}, limit=${batchSize}`); try { const response = await this.authenticatedFetch(`/api/trees?limit=${batchSize}&offset=${offset}`); if (!response) { console.error('Failed to fetch batch - no response'); break; } if (!response.ok) { console.error('API response not ok:', response.status, response.statusText); break; } const batchTrees = await response.json(); console.log(`Loaded batch: ${batchTrees.length} trees`); if (batchTrees.length === 0) { console.log('No more trees to load'); hasMoreTrees = false; break; } allTrees = allTrees.concat(batchTrees); offset += batchSize; // If we got less than the batch size, we've reached the end if (batchTrees.length < batchSize) { console.log(`Last batch loaded (${batchTrees.length} < ${batchSize})`); hasMoreTrees = false; } // Update progress if (treeCountEl) { treeCountEl.textContent = `${allTrees.length} trees loaded...`; } } catch (fetchError) { console.error('Error in batch fetch:', fetchError); break; } } console.log(`✅ Total trees loaded: ${allTrees.length} (via service_role backend)`); // Cache the fresh tree data localStorage.setItem(cacheKey, JSON.stringify(allTrees)); localStorage.setItem(`${cacheKey}_timestamp`, Date.now().toString()); console.log(`💾 Cached ${allTrees.length} trees to localStorage`); this.renderTreesToMap(allTrees); } catch (error) { console.error('Error loading trees:', error); this.showMessage('Failed to load trees. Please refresh the page.', 'error'); } } renderTreesToMap(allTrees) { // Filter out specific trees (e.g., ID 18 as requested) const filteredTrees = allTrees.filter(tree => { if (tree.id === 18) { console.log('Excluding tree ID 18 from display'); return false; } return true; }); console.log(`Total trees to display: ${filteredTrees.length}`); // Add tree markers with optimized batch processing let loadedCount = 0; const uiBatchSize = 100; // UI batch size for smooth rendering for (let i = 0; i < filteredTrees.length; i += uiBatchSize) { const batch = filteredTrees.slice(i, i + uiBatchSize); // Process batch with small delay to prevent UI blocking setTimeout(() => { batch.forEach((tree) => { try { this.addTreeToMap(tree); loadedCount++; } catch (error) { console.warn(`Failed to add marker for tree ${tree.id}:`, error); } }); // Update progress if (document.getElementById('treeCount')) { document.getElementById('treeCount').textContent = `${loadedCount} trees`; } // If this is the last batch, finalize if (i + uiBatchSize >= filteredTrees.length) { console.log(`Map loading complete: ${loadedCount} tree markers added`); // Auto-center on trees with clustering view setTimeout(() => { if (loadedCount > 0) { const group = new L.featureGroup(this.treeMarkers); const bounds = group.getBounds(); this.map.fitBounds(bounds.pad(0.1), { maxZoom: 15 }); console.log('Map auto-centered with clustering view'); } }, 100); } }, (i / uiBatchSize) * 50); // Stagger UI batches by 50ms } } addTreeToMap(tree) { // Use consistent forest green colors for all trees const colors = { canopy1: '#22c55e', // Forest green canopy2: '#16a34a', // Darker green canopy3: '#15803d', // Darkest green trunk: '#8b5a2b', // Brown trunk shadow: 'rgba(34, 197, 94, 0.3)' }; // Choose marker style - you can change this to test different options const markerStyle = 'realistic-tree'; // Options: 'realistic-tree', 'simple-circle', 'pin-style', 'geometric', 'minimalist', 'minimal-tree' let treeIcon; if (markerStyle === 'simple-circle') { treeIcon = L.divIcon({ html: `
`, className: 'custom-marker-icon simple-circle', iconSize: [20, 20], iconAnchor: [10, 10], popupAnchor: [0, -10] }); } else if (markerStyle === 'pin-style') { treeIcon = L.divIcon({ html: `
T
`, className: 'custom-marker-icon pin-style', iconSize: [25, 35], iconAnchor: [12, 35], popupAnchor: [0, -35] }); } else if (markerStyle === 'geometric') { treeIcon = L.divIcon({ html: `
`, className: 'custom-marker-icon geometric', iconSize: [24, 24], iconAnchor: [12, 24], popupAnchor: [0, -24] }); } else if (markerStyle === 'minimalist') { treeIcon = L.divIcon({ html: `
T
`, className: 'custom-marker-icon minimalist', iconSize: [16, 16], iconAnchor: [8, 8], popupAnchor: [0, -8] }); } else if (markerStyle === 'minimal-tree') { treeIcon = L.divIcon({ html: `
`, className: 'custom-marker-icon minimal-tree', iconSize: [28, 32], iconAnchor: [14, 30], popupAnchor: [0, -32] }); } else { // Default realistic tree style treeIcon = L.divIcon({ html: `
`, className: 'custom-marker-icon tree-pin realistic-tree', iconSize: [40, 48], iconAnchor: [20, 46], popupAnchor: [0, -48] }); } const marker = L.marker([tree.latitude, tree.longitude], { icon: treeIcon }); // Add to cluster group instead of directly to map for better performance this.markerClusterGroup.addLayer(marker); // Enhanced tooltip const treeName = tree.scientific_name || tree.common_name || tree.local_name || 'Unknown Tree'; const tooltipContent = `
${treeName}
ID: ${tree.id}${tree.tree_code ? ' | ' + tree.tree_code : ''}
${tree.created_by ? `
by ${tree.created_by}
` : ''}
`; marker.bindTooltip(tooltipContent, { permanent: false, direction: 'top', offset: [0, -10], className: 'tree-tooltip' }); // Enhanced popup with action buttons const canEdit = this.canEditTree(tree.created_by); const canDelete = this.canDeleteTree(tree.created_by); const popupContent = `

${treeName}

Tree ID: #${tree.id}${tree.tree_code ? ' (' + tree.tree_code + ')' : ''}
${tree.local_name ? `Local:${tree.local_name}` : ''} ${tree.scientific_name ? `Scientific:${tree.scientific_name}` : ''} ${tree.common_name ? `Common:${tree.common_name}` : ''} Location:${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)} ${tree.height ? `Height:${tree.height} ft` : ''} ${tree.width ? `Girth:${tree.width} ft` : ''} Added by:${tree.created_by || 'Unknown'} Date:${new Date(tree.created_at).toLocaleDateString()}
${tree.notes ? `
Notes:
${tree.notes}
` : ''} ${canEdit || canDelete ? `
${canEdit ? ` ` : ''} ${canDelete ? ` ` : ''}
` : ''}
`; marker.bindPopup(popupContent, { maxWidth: 320, minWidth: 280, className: 'tree-popup', closeButton: true, autoClose: true, autoPan: true, closeOnEscapeKey: true }); this.treeMarkers.push(marker); } clearTreeMarkers() { // Clear clustered markers if (this.markerClusterGroup) { this.markerClusterGroup.clearLayers(); } // Clear individual markers array this.treeMarkers.forEach(marker => { this.map.removeLayer(marker); }); this.treeMarkers = []; } // Tree management methods async editTree(treeId) { try { // Check if user has permission to edit (basic check) if (!this.currentUser) { this.showMessage('Authentication required', 'error'); return; } // Store tree ID in localStorage for the form localStorage.setItem('editTreeId', treeId.toString()); this.showMessage('Redirecting to form for editing...', 'success'); // Redirect to form page immediately setTimeout(() => { window.location.href = '/'; }, 500); } catch (error) { console.error('Error setting up tree for edit:', error); this.showMessage('Error preparing tree for editing: ' + error.message, 'error'); } } async deleteTree(treeId) { if (!confirm(`Are you sure you want to delete Tree #${treeId}? This action cannot be undone.`)) { return; } try { const response = await this.authenticatedFetch(`/api/trees/${treeId}`, { method: 'DELETE' }); if (!response) return; if (response.ok) { this.showMessage(`Tree #${treeId} deleted successfully`, 'success'); // Reload trees to update the map setTimeout(() => { this.loadTrees(); }, 1000); // Close any open popups this.map.closePopup(); } else { const error = await response.json(); this.showMessage('Error deleting tree: ' + (error.detail || 'Unknown error'), 'error'); } } catch (error) { console.error('Error deleting tree:', error); this.showMessage('Network error: ' + error.message, 'error'); } } showLoading() { document.getElementById('loading').style.display = 'block'; } hideLoading() { document.getElementById('loading').style.display = 'none'; } showMessage(message, type = 'success') { const messageElement = document.getElementById('message'); messageElement.textContent = message; messageElement.className = `message ${type} show`; setTimeout(() => { messageElement.classList.remove('show'); }, 3000); } showGestureHint() { const hint = document.querySelector('.gesture-hint'); if (hint) { hint.style.display = 'block'; setTimeout(() => { hint.style.display = 'none'; }, 4000); } } } // Initialize map when DOM is loaded let mapApp; document.addEventListener('DOMContentLoaded', () => { console.log('🗺️ DOM loaded, initializing TreeTrack Map...'); mapApp = new TreeTrackMap(); // Expose globally for popup button callbacks window.mapApp = mapApp; }); // Note: ES6 exports removed for traditional script loading