// TreeTrack Enhanced Map with Authentication and Tree Management class TreeTrackMap { constructor() { this.map = null; this.tempMarker = null; this.selectedLocation = null; this.treeMarkers = []; this.userLocation = null; this.isLocationSelected = false; // Authentication properties this.currentUser = null; this.authToken = null; this.init(); } async init() { // Check authentication first if (!await this.checkAuthentication()) { window.location.href = '/login'; return; } this.showLoading(); try { await this.initializeMap(); this.setupEventListeners(); await this.loadTrees(); this.setupUserInterface(); setTimeout(() => { this.hideLoading(); this.showGestureHint(); }, 1000); } catch (error) { console.error('Map initialization failed:', error); this.showMessage('Failed to initialize map. Please refresh the page.', 'error'); this.hideLoading(); } } // Authentication methods async checkAuthentication() { const token = localStorage.getItem('auth_token'); if (!token) { return false; } try { const response = await fetch('/api/auth/validate', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const data = await response.json(); this.currentUser = data.user; this.authToken = token; return true; } else { // 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(); } 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()); } } 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...'); // Default location (you can change this to your preferred location) const defaultLocation = [26.2006, 92.9376]; // Guwahati, Assam // Initialize map this.map = L.map('map', { center: defaultLocation, zoom: 13, zoomControl: true, attributionControl: true }); // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 18 }).addTo(this.map); // Map click handler for pin dropping this.map.on('click', (e) => { this.onMapClick(e); }); console.log('Map initialized successfully'); } 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 const group = new L.featureGroup(this.treeMarkers); this.map.fitBounds(group.getBounds().pad(0.1)); 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('Loading trees...'); try { const response = await this.authenticatedFetch('/api/trees?limit=1000'); if (!response) return; const trees = await response.json(); console.log(`Loaded ${trees.length} trees`); // Clear existing tree markers this.clearTreeMarkers(); // Add tree markers trees.forEach(tree => { this.addTreeMarker(tree); }); // Update tree count document.getElementById('treeCount').textContent = trees.length; if (trees.length > 0) { // Fit map to show all trees with debounced execution setTimeout(() => { const group = new L.featureGroup(this.treeMarkers); this.map.fitBounds(group.getBounds().pad(0.1)); }, 100); } } catch (error) { console.error('Error loading trees:', error); this.showMessage('Failed to load trees', 'error'); } } addTreeMarker(tree) { // Determine tree health/status color const getTreeColors = () => { // Default healthy tree colors using our new soft palette return { canopy1: '#8ab070', // primary-500 canopy2: '#739b5a', // primary-600 canopy3: '#5d7f49', // primary-700 trunk: '#7f5a44', // accent-700 shadow: 'rgba(93, 127, 73, 0.3)' }; }; const colors = getTreeColors(); const 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 }).addTo(this.map); // 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 (no emojis) 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() { 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(); });