// 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 ? `
` : ''}
${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();
});