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