// Global configuration
const CONFIG = {
DEFAULT_LAT: parseFloat(document.querySelector('meta[name="default-lat"]')?.content) || 53.5461,
DEFAULT_LNG: parseFloat(document.querySelector('meta[name="default-lng"]')?.content) || -113.4938,
DEFAULT_ZOOM: parseInt(document.querySelector('meta[name="default-zoom"]')?.content) || 11,
REFRESH_INTERVAL: 30000, // 30 seconds
MAX_ZOOM: 19,
MIN_ZOOM: 2
};
// Application state
let map = null;
let markers = [];
let userLocationMarker = null;
let isAddingLocation = false;
let refreshInterval = null;
let currentEditingLocation = null;
let currentUser = null;
let startLocationMarker = null;
let isStartLocationVisible = true;
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing application...');
checkAuth();
initializeMap();
loadLocations();
setupEventListeners();
setupAutoRefresh();
hideLoading();
});
// Check authentication
async function checkAuth() {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
if (!data.authenticated) {
window.location.href = '/login.html';
return;
}
currentUser = data.user;
updateUserInterface();
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/login.html';
}
}
// Update UI based on user
function updateUserInterface() {
if (!currentUser) return;
// Add user info and admin link to header if admin
const headerActions = document.querySelector('.header-actions');
if (currentUser.isAdmin && headerActions) {
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.className = 'btn btn-secondary';
adminLink.textContent = '⚙️ Admin';
headerActions.insertBefore(adminLink, headerActions.firstChild);
}
}
// Initialize the map
async function initializeMap() {
try {
// Get start location from server
const response = await fetch('/api/admin/start-location');
const data = await response.json();
let startLat = CONFIG.DEFAULT_LAT;
let startLng = CONFIG.DEFAULT_LNG;
let startZoom = CONFIG.DEFAULT_ZOOM;
if (data.success && data.location) {
startLat = data.location.latitude;
startLng = data.location.longitude;
startZoom = data.location.zoom;
}
// Initialize map
map = L.map('map').setView([startLat, startLng], startZoom);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: CONFIG.MAX_ZOOM,
minZoom: CONFIG.MIN_ZOOM
}).addTo(map);
// Add start location marker
addStartLocationMarker(startLat, startLng);
console.log('Map initialized successfully');
} catch (error) {
console.error('Failed to initialize map:', error);
showStatus('Failed to initialize map', 'error');
}
}
// Add start location marker function
function addStartLocationMarker(lat, lng) {
console.log(`Adding start location marker at: ${lat}, ${lng}`);
// Remove existing start location marker if it exists
if (startLocationMarker) {
map.removeLayer(startLocationMarker);
}
// Create a very distinctive custom icon
const startIcon = L.divIcon({
html: `
`,
className: 'start-location-custom-marker',
iconSize: [48, 48],
iconAnchor: [24, 48],
popupAnchor: [0, -48]
});
// Create the marker
startLocationMarker = L.marker([lat, lng], {
icon: startIcon,
zIndexOffset: 1000
}).addTo(map);
// Add popup
startLocationMarker.bindPopup(`
`);
}
// Toggle start location visibility
function toggleStartLocationVisibility() {
if (!startLocationMarker) return;
isStartLocationVisible = !isStartLocationVisible;
if (isStartLocationVisible) {
map.addLayer(startLocationMarker);
document.querySelector('#toggle-start-location-btn .btn-text').textContent = 'Hide Start Location';
} else {
map.removeLayer(startLocationMarker);
document.querySelector('#toggle-start-location-btn .btn-text').textContent = 'Show Start Location';
}
}
// Load locations from the API
async function loadLocations() {
try {
const response = await fetch('/api/locations');
const data = await response.json();
if (data.success) {
displayLocations(data.locations);
updateLocationCount(data.locations.length);
} else {
throw new Error(data.error || 'Failed to load locations');
}
} catch (error) {
console.error('Error loading locations:', error);
showStatus('Failed to load locations', 'error');
}
}
// Display locations on the map
function displayLocations(locations) {
// Clear existing markers
markers.forEach(marker => map.removeLayer(marker));
markers = [];
// Add new markers
locations.forEach(location => {
if (location.latitude && location.longitude) {
const marker = createLocationMarker(location);
markers.push(marker);
}
});
console.log(`Displayed ${markers.length} locations`);
}
// Create a location marker
function createLocationMarker(location) {
const lat = parseFloat(location.latitude);
const lng = parseFloat(location.longitude);
// Determine marker color based on support level
let markerColor = 'blue';
if (location['Support Level']) {
const level = parseInt(location['Support Level']);
switch(level) {
case 1: markerColor = 'green'; break;
case 2: markerColor = 'yellow'; break;
case 3: markerColor = 'orange'; break;
case 4: markerColor = 'red'; break;
}
}
const marker = L.circleMarker([lat, lng], {
radius: 8,
fillColor: markerColor,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
}).addTo(map);
// Create popup content
const popupContent = createPopupContent(location);
marker.bindPopup(popupContent);
// Add click handler for editing
marker.on('click', () => {
if (currentUser) {
setTimeout(() => openEditForm(location), 100);
}
});
return marker;
}
// Create popup content
function createPopupContent(location) {
const name = [location['First Name'], location['Last Name']]
.filter(Boolean).join(' ') || 'Unknown';
const address = location.Address || 'No address';
const supportLevel = location['Support Level'] ?
`Level ${location['Support Level']}` : 'Not specified';
return `
`;
}
// Setup event listeners
function setupEventListeners() {
// Refresh button
document.getElementById('refresh-btn')?.addEventListener('click', () => {
loadLocations();
showStatus('Locations refreshed', 'success');
});
// Geolocate button
document.getElementById('geolocate-btn')?.addEventListener('click', getUserLocation);
// Toggle start location button
document.getElementById('toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility);
// Add location button
document.getElementById('add-location-btn')?.addEventListener('click', toggleAddLocationMode);
// Fullscreen button
document.getElementById('fullscreen-btn')?.addEventListener('click', toggleFullscreen);
// Modal controls
document.getElementById('close-modal-btn')?.addEventListener('click', closeAddModal);
document.getElementById('cancel-modal-btn')?.addEventListener('click', closeAddModal);
// Edit footer controls
document.getElementById('close-edit-footer-btn')?.addEventListener('click', closeEditForm);
// Forms
document.getElementById('location-form')?.addEventListener('submit', handleAddLocation);
document.getElementById('edit-location-form')?.addEventListener('submit', handleEditLocation);
// Delete button
document.getElementById('delete-location-btn')?.addEventListener('click', handleDeleteLocation);
// Address lookup buttons
document.getElementById('lookup-address-add-btn')?.addEventListener('click', () => {
lookupAddress('add');
});
document.getElementById('lookup-address-edit-btn')?.addEventListener('click', () => {
lookupAddress('edit');
});
// Geo-location field sync
setupGeoLocationSync();
}
// Setup geo-location field synchronization
function setupGeoLocationSync() {
// For add form
const addLatInput = document.getElementById('location-lat');
const addLngInput = document.getElementById('location-lng');
const addGeoInput = document.getElementById('geo-location');
if (addLatInput && addLngInput && addGeoInput) {
[addLatInput, addLngInput].forEach(input => {
input.addEventListener('input', () => {
const lat = addLatInput.value;
const lng = addLngInput.value;
if (lat && lng) {
addGeoInput.value = `${lat};${lng}`;
}
});
});
addGeoInput.addEventListener('input', () => {
const coords = parseGeoLocation(addGeoInput.value);
if (coords) {
addLatInput.value = coords.lat;
addLngInput.value = coords.lng;
}
});
}
// For edit form
const editLatInput = document.getElementById('edit-location-lat');
const editLngInput = document.getElementById('edit-location-lng');
const editGeoInput = document.getElementById('edit-geo-location');
if (editLatInput && editLngInput && editGeoInput) {
[editLatInput, editLngInput].forEach(input => {
input.addEventListener('input', () => {
const lat = editLatInput.value;
const lng = editLngInput.value;
if (lat && lng) {
editGeoInput.value = `${lat};${lng}`;
}
});
});
editGeoInput.addEventListener('input', () => {
const coords = parseGeoLocation(editGeoInput.value);
if (coords) {
editLatInput.value = coords.lat;
editLngInput.value = coords.lng;
}
});
}
}
// Parse geo-location string
function parseGeoLocation(value) {
if (!value) return null;
// Try semicolon separator first
let parts = value.split(';');
if (parts.length !== 2) {
// Try comma separator
parts = value.split(',');
}
if (parts.length === 2) {
const lat = parseFloat(parts[0].trim());
const lng = parseFloat(parts[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
return { lat, lng };
}
}
return null;
}
// Get user location
function getUserLocation() {
if (!navigator.geolocation) {
showStatus('Geolocation is not supported by your browser', 'error');
return;
}
showStatus('Getting your location...', 'info');
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
// Center map on user location
map.setView([lat, lng], 15);
// Add or update user location marker
if (userLocationMarker) {
userLocationMarker.setLatLng([lat, lng]);
} else {
userLocationMarker = L.circleMarker([lat, lng], {
radius: 10,
fillColor: '#2196F3',
color: '#fff',
weight: 3,
opacity: 1,
fillOpacity: 0.8
}).addTo(map);
userLocationMarker.bindPopup('Your Location');
}
showStatus('Location found!', 'success');
},
(error) => {
let message = 'Unable to get your location';
switch(error.code) {
case error.PERMISSION_DENIED:
message = 'Location permission denied';
break;
case error.POSITION_UNAVAILABLE:
message = 'Location information unavailable';
break;
case error.TIMEOUT:
message = 'Location request timed out';
break;
}
showStatus(message, 'error');
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
}
// Toggle add location mode
function toggleAddLocationMode() {
isAddingLocation = !isAddingLocation;
const crosshair = document.getElementById('crosshair');
const addBtn = document.getElementById('add-location-btn');
if (isAddingLocation) {
crosshair.classList.remove('hidden');
addBtn.classList.add('active');
addBtn.innerHTML = '✕Cancel';
map.on('click', handleMapClick);
} else {
crosshair.classList.add('hidden');
addBtn.classList.remove('active');
addBtn.innerHTML = '➕Add Location Here';
map.off('click', handleMapClick);
}
}
// Handle map click in add mode
function handleMapClick(e) {
if (!isAddingLocation) return;
const { lat, lng } = e.latlng;
openAddModal(lat, lng);
toggleAddLocationMode();
}
// Open add location modal
function openAddModal(lat, lng) {
const modal = document.getElementById('add-modal');
const latInput = document.getElementById('location-lat');
const lngInput = document.getElementById('location-lng');
const geoInput = document.getElementById('geo-location');
// Set coordinates
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
// Clear other fields
document.getElementById('location-form').reset();
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
// Show modal
modal.classList.remove('hidden');
}
// Close add modal
function closeAddModal() {
const modal = document.getElementById('add-modal');
modal.classList.add('hidden');
document.getElementById('location-form').reset();
}
// Handle add location form submission
async function handleAddLocation(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = {};
// Convert form data to object
for (let [key, value] of formData.entries()) {
if (value.trim() !== '') {
data[key] = value.trim();
}
}
// Ensure geo-location is set
if (data.latitude && data.longitude) {
data['Geo-Location'] = `${data.latitude};${data.longitude}`;
}
// Handle checkbox
data.Sign = document.getElementById('sign').checked;
try {
const response = await fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showStatus('Location added successfully!', 'success');
closeAddModal();
loadLocations();
} else {
throw new Error(result.error || 'Failed to add location');
}
} catch (error) {
console.error('Error adding location:', error);
showStatus(error.message || 'Failed to add location', 'error');
}
}
// Open edit form
function openEditForm(location) {
currentEditingLocation = location;
// Populate form fields
document.getElementById('edit-location-id').value = location.Id || '';
document.getElementById('edit-first-name').value = location['First Name'] || '';
document.getElementById('edit-last-name').value = location['Last Name'] || '';
document.getElementById('edit-location-email').value = location.Email || '';
document.getElementById('edit-location-phone').value = location.Phone || '';
document.getElementById('edit-location-unit').value = location['Unit Number'] || '';
document.getElementById('edit-support-level').value = location['Support Level'] || '';
document.getElementById('edit-location-address').value = location.Address || '';
document.getElementById('edit-sign').checked = location.Sign === true || location.Sign === 'true';
document.getElementById('edit-sign-size').value = location['Sign Size'] || '';
document.getElementById('edit-location-notes').value = location.Notes || '';
document.getElementById('edit-location-lat').value = location.latitude || '';
document.getElementById('edit-location-lng').value = location.longitude || '';
document.getElementById('edit-geo-location').value = location['Geo-Location'] || '';
// Show edit footer
document.getElementById('edit-footer').classList.remove('hidden');
}
// Close edit form
function closeEditForm() {
document.getElementById('edit-footer').classList.add('hidden');
currentEditingLocation = null;
}
// Handle edit location form submission
async function handleEditLocation(e) {
e.preventDefault();
if (!currentEditingLocation) return;
const formData = new FormData(e.target);
const data = { Id: currentEditingLocation.Id };
// Convert form data to object
for (let [key, value] of formData.entries()) {
if (value.trim() !== '') {
data[key] = value.trim();
}
}
// Ensure geo-location is set
if (data.latitude && data.longitude) {
data['Geo-Location'] = `${data.latitude};${data.longitude}`;
}
// Handle checkbox
data.Sign = document.getElementById('edit-sign').checked;
try {
const response = await fetch(`/api/locations/${currentEditingLocation.Id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showStatus('Location updated successfully!', 'success');
closeEditForm();
loadLocations();
} else {
throw new Error(result.error || 'Failed to update location');
}
} catch (error) {
console.error('Error updating location:', error);
showStatus(error.message || 'Failed to update location', 'error');
}
}
// Handle delete location
async function handleDeleteLocation() {
if (!currentEditingLocation) return;
if (!confirm('Are you sure you want to delete this location?')) {
return;
}
try {
const response = await fetch(`/api/locations/${currentEditingLocation.Id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showStatus('Location deleted successfully!', 'success');
closeEditForm();
loadLocations();
} else {
throw new Error(result.error || 'Failed to delete location');
}
} catch (error) {
console.error('Error deleting location:', error);
showStatus(error.message || 'Failed to delete location', 'error');
}
}
// Lookup address
async function lookupAddress(mode) {
const addressInput = mode === 'add' ?
document.getElementById('location-address') :
document.getElementById('edit-location-address');
const address = addressInput.value.trim();
if (!address) {
showStatus('Please enter an address to lookup', 'warning');
return;
}
try {
showStatus('Looking up address...', 'info');
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1`);
const results = await response.json();
if (results.length > 0) {
const result = results[0];
const lat = parseFloat(result.lat);
const lng = parseFloat(result.lon);
// Update form fields
if (mode === 'add') {
document.getElementById('location-lat').value = lat.toFixed(8);
document.getElementById('location-lng').value = lng.toFixed(8);
document.getElementById('geo-location').value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
} else {
document.getElementById('edit-location-lat').value = lat.toFixed(8);
document.getElementById('edit-location-lng').value = lng.toFixed(8);
document.getElementById('edit-geo-location').value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
}
// Center map on location
map.setView([lat, lng], 16);
showStatus('Address found!', 'success');
} else {
showStatus('Address not found. Please try a different format.', 'warning');
}
} catch (error) {
console.error('Address lookup error:', error);
showStatus('Failed to lookup address', 'error');
}
}
// Toggle fullscreen
function toggleFullscreen() {
const app = document.getElementById('app');
const btn = document.getElementById('fullscreen-btn');
if (!document.fullscreenElement) {
app.requestFullscreen().then(() => {
app.classList.add('fullscreen');
btn.innerHTML = '◱Exit Fullscreen';
}).catch(err => {
console.error('Error entering fullscreen:', err);
showStatus('Unable to enter fullscreen', 'error');
});
} else {
document.exitFullscreen().then(() => {
app.classList.remove('fullscreen');
btn.innerHTML = '⛶Fullscreen';
});
}
}
// Update location count
function updateLocationCount(count) {
const countElement = document.getElementById('location-count');
if (countElement) {
countElement.textContent = `${count} location${count !== 1 ? 's' : ''}`;
}
}
// Setup auto-refresh
function setupAutoRefresh() {
refreshInterval = setInterval(() => {
loadLocations();
}, CONFIG.REFRESH_INTERVAL);
}
// Show status message
function showStatus(message, type = 'info') {
const container = document.getElementById('status-container');
const messageDiv = document.createElement('div');
messageDiv.className = `status-message ${type}`;
messageDiv.textContent = message;
container.appendChild(messageDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
messageDiv.remove();
}, 5000);
}
// Hide loading overlay
function hideLoading() {
const loading = document.getElementById('loading');
if (loading) {
loading.classList.add('hidden');
}
}
// Escape HTML for security
function escapeHtml(text) {
if (text === null || text === undefined) {
return '';
}
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});