Spaces:
Running
Running
// 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: ` | |
<div class="map-marker temp-marker" style="filter: drop-shadow(2px 3px 6px ${tempColors.shadow});"> | |
<svg width="36" height="44" viewBox="0 0 36 44" fill="none" style="transition: transform 0.2s ease;"> | |
<!-- Tree Shadow/Base --> | |
<ellipse cx="18" cy="42" rx="7" ry="1.5" fill="${tempColors.shadow}" opacity="0.4"/> | |
<!-- Tree Trunk --> | |
<path d="M16 35 Q16 37 16.5 39 Q17 41 17.5 42 Q18 42.5 18 42.5 Q18 42.5 18.5 42 Q19 41 19.5 39 Q20 37 20 35 L20 29 Q19.8 28 19 27.5 Q18 27 18 27 Q18 27 17 27.5 Q16.2 28 16 29 Z" fill="${tempColors.trunk}" stroke="#6b4a39" stroke-width="0.4"/> | |
<!-- Tree Trunk Texture --> | |
<path d="M17 29 Q17 32 17 35" stroke="#5a3e32" stroke-width="0.2" opacity="0.6"/> | |
<path d="M19 30 Q19 33 19 36" stroke="#5a3e32" stroke-width="0.2" opacity="0.6"/> | |
<!-- Main Canopy (Back Layer) --> | |
<circle cx="18" cy="20" r="10" fill="${tempColors.canopy3}" opacity="0.8"/> | |
<!-- Secondary Canopy Clusters --> | |
<circle cx="14" cy="18" r="7" fill="${tempColors.canopy2}" opacity="0.85"/> | |
<circle cx="22" cy="17" r="6" fill="${tempColors.canopy2}" opacity="0.85"/> | |
<circle cx="16" cy="14" r="5" fill="${tempColors.canopy1}" opacity="0.9"/> | |
<circle cx="21" cy="22" r="5.5" fill="${tempColors.canopy2}" opacity="0.85"/> | |
<!-- Top Canopy (Brightest) --> | |
<circle cx="18" cy="16" r="6" fill="${tempColors.canopy1}"/> | |
<!-- Highlight clusters for 3D effect --> | |
<circle cx="15" cy="14" r="2.5" fill="#fca5a5" opacity="0.7"/> | |
<circle cx="21" cy="18" r="2" fill="#fca5a5" opacity="0.6"/> | |
<circle cx="16" cy="21" r="1.5" fill="#fca5a5" opacity="0.5"/> | |
<!-- Small light spots --> | |
<circle cx="13" cy="13" r="0.8" fill="#fee2e2" opacity="0.8"/> | |
<circle cx="20" cy="15" r="0.6" fill="#fee2e2" opacity="0.9"/> | |
<circle cx="23" cy="20" r="0.5" fill="#fee2e2" opacity="0.7"/> | |
</svg> | |
</div> | |
`, | |
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: ` | |
<div class="map-marker user-marker"> | |
<svg width="24" height="32" viewBox="0 0 24 32" fill="none"> | |
<path d="M12 0C5.37 0 0 5.37 0 12C0 21 12 32 12 32S24 21 24 12C24 5.37 18.63 0 12 0Z" fill="#3b82f6"/> | |
<circle cx="12" cy="12" r="4" fill="white"/> | |
</svg> | |
</div> | |
`, | |
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: ` | |
<div class="map-marker tree-marker" style="filter: drop-shadow(2px 3px 6px ${colors.shadow});"> | |
<svg width="40" height="48" viewBox="0 0 40 48" fill="none" style="transition: transform 0.2s ease;"> | |
<!-- Tree Shadow/Base --> | |
<ellipse cx="20" cy="46" rx="8" ry="2" fill="${colors.shadow}" opacity="0.4"/> | |
<!-- Tree Trunk --> | |
<path d="M17 38 Q17 40 17.5 42 Q18 44 19 45 Q19.5 45.5 20 45.5 Q20.5 45.5 21 45 Q22 44 22.5 42 Q23 40 23 38 L23 32 Q22.8 31 22 30.5 Q21 30 20 30 Q19 30 18 30.5 Q17.2 31 17 32 Z" fill="${colors.trunk}" stroke="#6b4a39" stroke-width="0.5"/> | |
<!-- Tree Trunk Texture --> | |
<path d="M18.5 32 Q18.5 35 18.5 38" stroke="#5a3e32" stroke-width="0.3" opacity="0.6"/> | |
<path d="M21.5 33 Q21.5 36 21.5 39" stroke="#5a3e32" stroke-width="0.3" opacity="0.6"/> | |
<!-- Main Canopy (Back Layer) --> | |
<circle cx="20" cy="22" r="12" fill="${colors.canopy3}" opacity="0.8"/> | |
<!-- Secondary Canopy Clusters --> | |
<circle cx="15" cy="20" r="8" fill="${colors.canopy2}" opacity="0.85"/> | |
<circle cx="25" cy="19" r="7" fill="${colors.canopy2}" opacity="0.85"/> | |
<circle cx="18" cy="15" r="6" fill="${colors.canopy1}" opacity="0.9"/> | |
<circle cx="23" cy="25" r="6.5" fill="${colors.canopy2}" opacity="0.85"/> | |
<!-- Top Canopy (Brightest) --> | |
<circle cx="20" cy="18" r="7" fill="${colors.canopy1}"/> | |
<!-- Highlight clusters for 3D effect --> | |
<circle cx="16" cy="15" r="3" fill="#a8b9a0" opacity="0.7"/> | |
<circle cx="24" cy="20" r="2.5" fill="#a8b9a0" opacity="0.6"/> | |
<circle cx="18" cy="24" r="2" fill="#a8b9a0" opacity="0.5"/> | |
<!-- Small light spots --> | |
<circle cx="14" cy="14" r="1" fill="#c0d4b2" opacity="0.8"/> | |
<circle cx="22" cy="16" r="0.8" fill="#c0d4b2" opacity="0.9"/> | |
<circle cx="26" cy="22" r="0.6" fill="#c0d4b2" opacity="0.7"/> | |
<!-- Optional: Small leaves/details --> | |
<path d="M12 18 Q11 17 11.5 19 Q12.5 20 13 19" fill="${colors.canopy1}" opacity="0.6"/> | |
<path d="M28 25 Q29 24 28.5 26 Q27.5 27 27 26" fill="${colors.canopy1}" opacity="0.6"/> | |
</svg> | |
</div> | |
`, | |
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 = ` | |
<div class="tree-tooltip-content"> | |
<div class="tree-name">${treeName}</div> | |
<div class="tree-details">ID: ${tree.id}${tree.tree_code ? ' | ' + tree.tree_code : ''}</div> | |
${tree.created_by ? `<div class="tree-creator">by ${tree.created_by}</div>` : ''} | |
</div> | |
`; | |
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 = ` | |
<div style="width: 300px; max-width: 90vw; font-family: 'Segoe UI', sans-serif; position: relative;"> | |
<div style="padding: 20px; padding-bottom: 16px;"> | |
<div style="display: flex; justify-content: between; align-items: flex-start; margin-bottom: 16px;"> | |
<div style="flex: 1;"> | |
<h3 style="margin: 0 0 8px 0; color: #1e40af; font-size: 18px; font-weight: 600; line-height: 1.2; word-wrap: break-word;"> | |
${treeName} | |
</h3> | |
<div style="color: #6b7280; font-size: 13px;"> | |
<strong>Tree ID:</strong> #${tree.id}${tree.tree_code ? ' (' + tree.tree_code + ')' : ''} | |
</div> | |
</div> | |
</div> | |
<div style="margin-bottom: 16px;"> | |
<div style="display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; font-size: 13px;"> | |
${tree.local_name ? `<strong style="color: #374151;">Local:</strong><span style="word-wrap: break-word;">${tree.local_name}</span>` : ''} | |
${tree.scientific_name ? `<strong style="color: #374151;">Scientific:</strong><span style="word-wrap: break-word;"><em>${tree.scientific_name}</em></span>` : ''} | |
${tree.common_name ? `<strong style="color: #374151;">Common:</strong><span style="word-wrap: break-word;">${tree.common_name}</span>` : ''} | |
<strong style="color: #374151;">Location:</strong><span style="font-family: monospace; font-size: 12px;">${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}</span> | |
${tree.height ? `<strong style="color: #374151;">Height:</strong><span>${tree.height} ft</span>` : ''} | |
${tree.width ? `<strong style="color: #374151;">Girth:</strong><span>${tree.width} ft</span>` : ''} | |
<strong style="color: #374151;">Added by:</strong><span>${tree.created_by || 'Unknown'}</span> | |
<strong style="color: #374151;">Date:</strong><span>${new Date(tree.created_at).toLocaleDateString()}</span> | |
</div> | |
</div> | |
${tree.notes ? ` | |
<div style="margin-bottom: 16px; padding: 12px; background: #f8fafc; border-radius: 6px; border-left: 4px solid #e2e8f0;"> | |
<strong style="color: #374151; font-size: 13px; display: block; margin-bottom: 6px;">Notes:</strong> | |
<div style="color: #6b7280; font-size: 12px; line-height: 1.4; word-wrap: break-word;"> | |
${tree.notes} | |
</div> | |
</div> | |
` : ''} | |
${canEdit || canDelete ? ` | |
<div style="display: flex; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb;"> | |
${canEdit ? ` | |
<button onclick="mapApp.editTree(${tree.id})" | |
style="flex: 1; background: #3b82f6; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s; min-height: 36px;" | |
onmouseover="this.style.backgroundColor='#2563eb'; this.style.transform='translateY(-1px)';" | |
onmouseout="this.style.backgroundColor='#3b82f6'; this.style.transform='translateY(0)';"> | |
✏️ Edit | |
</button> | |
` : ''} | |
${canDelete ? ` | |
<button onclick="mapApp.deleteTree(${tree.id})" | |
style="flex: 1; background: #ef4444; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s; min-height: 36px;" | |
onmouseover="this.style.backgroundColor='#dc2626'; this.style.transform='translateY(-1px)';" | |
onmouseout="this.style.backgroundColor='#ef4444'; this.style.transform='translateY(0)';"> | |
🗑️ Delete | |
</button> | |
` : ''} | |
</div> | |
` : ''} | |
</div> | |
</div> | |
`; | |
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(); | |
}); | |