// TM GNSS Guard - Dashboard JavaScript // Auto-refreshes data every 10 seconds and updates the UI let map = null; let markers = {}; let currentData = null; let lastFetchSucceeded = false; let lastValidationTimestamp = null; // Route visualization let routeMarkers = []; let routeCoords = []; // Store route coordinates for bounds calculation let showRouteEnabled = getRoutePreference(); // Load from localStorage let isInitialMapLoad = true; // Only fit bounds on initial load // Get route preference from localStorage (defaults to true if not set) function getRoutePreference() { if (!window.SHOW_ROUTE) return false; const stored = localStorage.getItem('showRoute'); return stored === null ? true : stored === 'true'; } // Save route preference to localStorage function saveRoutePreference(enabled) { localStorage.setItem('showRoute', enabled ? 'true' : 'false'); } // ============================================================================= // AUTO-REFRESH PAGE (every 1 hour to pick up deployments) // ============================================================================= const PAGE_LOAD_TIME = Date.now(); const AUTO_REFRESH_INTERVAL_MS = 60 * 60 * 1000; // 1 hour let lastVisibilityCheck = Date.now(); function checkAutoRefresh() { const elapsed = Date.now() - PAGE_LOAD_TIME; if (elapsed >= AUTO_REFRESH_INTERVAL_MS) { console.log('Auto-refreshing page after 1 hour...'); window.location.reload(); } } // Check for refresh on visibility change (tab becomes active) document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { const now = Date.now(); // Only check if at least 10 seconds since last check (prevents rapid refreshes) if (now - lastVisibilityCheck > 10000) { lastVisibilityCheck = now; checkAutoRefresh(); } } }); // Periodic check every 5 minutes while tab is active setInterval(checkAutoRefresh, 5 * 60 * 1000); // Initialize map function initMap() { // Create map centered on default location map = L.map('map', { zoomControl: true }).setView([34.665151, 33.016326], 11); // Dark basemap L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© OpenStreetMap & CARTO' }).addTo(map); // Recalculate marker offsets when zoom changes map.on('zoomend', () => { if (currentData) { updateMap(currentData); } }); } // Create marker icons (using local files) function makeIcon(color) { return new L.Icon({ iconUrl: `/static/markers/marker-icon-${color}.png`, shadowUrl: '/static/markers/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34] }); } const iconPrimary = makeIcon('violet'); const iconPrimaryAlert = makeIcon('red'); const iconAIS = makeIcon('blue'); const iconStarlinkGps = makeIcon('yellow'); const iconStarlinkLocation = makeIcon('green'); const iconSecondary = makeIcon('grey'); // Format timestamp as relative time function formatRelativeTime(timestampUnix) { if (!timestampUnix) return '-'; const now = Date.now() / 1000; const diff = now - timestampUnix; if (diff < 0) return 'just now'; if (diff < 60) return `${Math.floor(diff)} sec ago`; if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`; return `${Math.floor(diff / 86400)} days ago`; } // Format UTC timestamp for display function formatUTCTimestamp(isoString) { if (!isoString) return '-'; const date = new Date(isoString); const day = String(date.getUTCDate()).padStart(2, '0'); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const year = date.getUTCFullYear(); const hours = String(date.getUTCHours()).padStart(2, '0'); const minutes = String(date.getUTCMinutes()).padStart(2, '0'); const seconds = String(date.getUTCSeconds()).padStart(2, '0'); return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`; } // Log event to event log function logEvent(level, text) { const eventLog = document.getElementById('eventLog'); if (!eventLog) return; const div = document.createElement('div'); div.className = 'event level-' + level; const spanLevel = document.createElement('span'); spanLevel.className = 'level'; spanLevel.textContent = level.toUpperCase(); div.appendChild(spanLevel); const spanText = document.createElement('span'); const timestamp = new Date().toTimeString().slice(0, 8); spanText.textContent = ' [' + timestamp + '] ' + text; div.appendChild(spanText); eventLog.appendChild(div); // Limit to last 3 events (re-query each iteration since querySelectorAll is static) while (eventLog.querySelectorAll('.event').length > 3) { const firstEvent = eventLog.querySelector('.event'); if (firstEvent) { eventLog.removeChild(firstEvent); } else { break; } } } // Update source card function updateSource(sourceName, sourceData) { const card = document.getElementById(`card-${sourceName}`); const badge = document.getElementById(`badge-${sourceName}`); const coordsEl = document.getElementById(`coords-${sourceName}`); const updateEl = document.getElementById(`update-${sourceName}`); if (!card || !badge) return; // Reset card classes card.classList.remove('ok', 'warn', 'crit', 'stale', 'offline'); badge.className = 'badge'; if (!sourceData.enabled) { // Not configured card.classList.add('offline'); badge.classList.add('badge-offline'); badge.textContent = 'NOT CONFIGURED'; if (coordsEl) { coordsEl.textContent = 'No data source configured.'; } if (updateEl) { updateEl.textContent = '-'; updateEl.className = ''; } } else if (sourceData.status === 'missing') { // Missing data card.classList.add('crit'); badge.classList.add('badge-danger'); badge.textContent = 'MISSING'; if (coordsEl) { coordsEl.textContent = 'MISSING'; coordsEl.className = 'alert'; } if (updateEl) { updateEl.textContent = formatRelativeTime(sourceData.last_update_unix); updateEl.className = 'alert'; } } else if (sourceData.status === 'stale' || sourceData.is_stale) { // Stale data - has coordinates but timestamp is old card.classList.add('stale'); badge.classList.add('badge-stale'); badge.textContent = 'STALE'; if (coordsEl && sourceData.coordinates) { const lat = sourceData.coordinates.latitude; const lon = sourceData.coordinates.longitude; coordsEl.textContent = `${lat.toFixed(6)}, ${lon.toFixed(6)}`; coordsEl.className = ''; } if (updateEl) { updateEl.textContent = formatRelativeTime(sourceData.last_update_unix); updateEl.className = 'stale-text'; } } else { // Has valid data card.classList.add('ok'); badge.classList.add('badge-healthy'); badge.textContent = 'HEALTHY'; if (coordsEl && sourceData.coordinates) { const lat = sourceData.coordinates.latitude; const lon = sourceData.coordinates.longitude; coordsEl.textContent = `${lat.toFixed(6)}, ${lon.toFixed(6)}`; coordsEl.className = ''; } if (updateEl) { updateEl.textContent = formatRelativeTime(sourceData.last_update_unix); updateEl.className = ''; } } } // Calculate offset for markers to spread them in a circle when close together function calculateMarkerOffsets(sourceCoords, zoomLevel) { if (Object.keys(sourceCoords).length <= 1) { // Single marker, no offset needed const result = {}; for (const [name, coord] of Object.entries(sourceCoords)) { result[name] = { lat: coord.lat, lon: coord.lon, offsetLat: 0, offsetLon: 0 }; } return result; } // Calculate centroid let sumLat = 0, sumLon = 0, count = 0; for (const coord of Object.values(sourceCoords)) { sumLat += coord.lat; sumLon += coord.lon; count++; } const centroidLat = sumLat / count; const centroidLon = sumLon / count; // Check if markers are close together (within ~50 meters) const closeThreshold = 0.0005; // ~50m in degrees let maxDist = 0; for (const coord of Object.values(sourceCoords)) { const dist = Math.sqrt( Math.pow(coord.lat - centroidLat, 2) + Math.pow(coord.lon - centroidLon, 2) ); maxDist = Math.max(maxDist, dist); } // If markers are spread out enough, don't offset if (maxDist > closeThreshold) { const result = {}; for (const [name, coord] of Object.entries(sourceCoords)) { result[name] = { lat: coord.lat, lon: coord.lon, offsetLat: 0, offsetLon: 0 }; } return result; } // Calculate offset radius based on zoom level (smaller offset when zoomed in) // At zoom 15, offset ~30m; at zoom 10, offset ~100m const baseOffset = 0.0003; // ~30m base offset const zoomFactor = Math.pow(2, 15 - Math.min(zoomLevel, 18)); const offsetRadius = baseOffset * zoomFactor; // Arrange markers in a circle around centroid const result = {}; const sourceNames = Object.keys(sourceCoords); const angleStep = (2 * Math.PI) / sourceNames.length; sourceNames.forEach((name, index) => { const angle = angleStep * index - Math.PI / 2; // Start from top const offsetLat = offsetRadius * Math.cos(angle); const offsetLon = offsetRadius * Math.sin(angle) * 1.5; // Adjust for latitude distortion result[name] = { lat: centroidLat + offsetLat, lon: centroidLon + offsetLon, offsetLat: offsetLat, offsetLon: offsetLon, originalLat: sourceCoords[name].lat, originalLon: sourceCoords[name].lon }; }); return result; } // Update map markers function updateMap(data) { if (!data.map_center) return; const sourceMapping = { 'nmea_primary': { icon: iconPrimary, name: 'Primary GPS' }, 'nmea_secondary': { icon: iconSecondary, name: 'Secondary GPS' }, 'tm_ais': { icon: iconAIS, name: 'TM AIS GPS' }, 'starlink_gps': { icon: iconStarlinkGps, name: 'Starlink GPS' }, 'starlink_location': { icon: iconStarlinkLocation, name: 'Starlink Location' } }; // Collect all valid source coordinates const sourceCoords = {}; const allCoords = []; Object.keys(sourceMapping).forEach(sourceName => { const sourceData = data.sources[sourceName]; if (sourceData && sourceData.enabled && sourceData.status !== 'missing' && sourceData.coordinates) { const lat = sourceData.coordinates.latitude; const lon = sourceData.coordinates.longitude; sourceCoords[sourceName] = { lat, lon }; allCoords.push([lat, lon]); } }); // Calculate offsets for overlapping markers const zoomLevel = map.getZoom() || 13; const offsetPositions = calculateMarkerOffsets(sourceCoords, zoomLevel); // Update or create markers for each source Object.keys(sourceMapping).forEach(sourceName => { const sourceData = data.sources[sourceName]; if (!sourceData || !sourceData.enabled || sourceData.status === 'missing' || !sourceData.coordinates) { // Remove marker if it exists if (markers[sourceName]) { map.removeLayer(markers[sourceName]); delete markers[sourceName]; } return; } const mapping = sourceMapping[sourceName]; const position = offsetPositions[sourceName]; // Use alert icon for primary if there's an alert let icon = mapping.icon; if (sourceName === 'nmea_primary' && data.has_alert && !data.is_valid) { icon = iconPrimaryAlert; } // Build popup with original coordinates const origLat = sourceData.coordinates.latitude; const origLon = sourceData.coordinates.longitude; const popupContent = `${mapping.name}
` + `Lat: ${origLat.toFixed(6)}
` + `Lon: ${origLon.toFixed(6)}`; if (markers[sourceName]) { markers[sourceName].setLatLng([position.lat, position.lon]).setIcon(icon); markers[sourceName].setPopupContent(popupContent); } else { markers[sourceName] = L.marker([position.lat, position.lon], { icon: icon }) .addTo(map) .bindPopup(popupContent); } }); // Fit map to show all markers (only on initial load, not on refresh) // If route is enabled, wait for route data to load before fitting bounds if (isInitialMapLoad && !window.SHOW_ROUTE) { if (allCoords.length > 0) { const bounds = L.latLngBounds(allCoords); map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); } else if (data.map_center && data.map_center.latitude && data.map_center.longitude) { map.setView([data.map_center.latitude, data.map_center.longitude], 13); } isInitialMapLoad = false; } } // Track alarm state let alarmActive = false; let alarmAcknowledged = false; // Update global status pill and border frame function updateGlobalStatus(data) { const statusPill = document.getElementById('globalStatusPill'); const statusText = statusPill ? statusPill.querySelector('.status-text') : null; const borderFrame = document.getElementById('healthBorderFrame'); if (statusPill) { statusPill.classList.remove('warn', 'crit', 'alarm-active', 'alarm-acknowledged'); } if (borderFrame) { borderFrame.classList.remove('warn', 'crit'); } if (data.is_valid) { if (statusText) { statusText.textContent = 'GNSS Integrity: Stable'; } else if (statusPill) { statusPill.textContent = 'GNSS Integrity: Stable'; } // Border frame hidden (no class) when healthy // Reset alarm state when status returns to healthy alarmActive = false; alarmAcknowledged = false; } else if (data.has_alert && data.max_distance_km !== null) { if (statusText) { statusText.textContent = 'GNSS Integrity: At Risk'; } else if (statusPill) { statusPill.textContent = 'GNSS Integrity: At Risk'; } if (statusPill) { statusPill.classList.add('crit'); } if (borderFrame) { borderFrame.classList.add('crit'); } // Alarm should be active in this state if (!alarmAcknowledged) { alarmActive = true; } } else { if (statusText) { statusText.textContent = 'GNSS Integrity: Degraded'; } else if (statusPill) { statusPill.textContent = 'GNSS Integrity: Degraded'; } if (statusPill) { statusPill.classList.add('warn'); } if (borderFrame) { borderFrame.classList.add('warn'); } // Alarm should be active in this state if (!alarmAcknowledged) { alarmActive = true; } } // Update alarm visual state updateAlarmVisualState(); } // Update alarm visual state on the button function updateAlarmVisualState() { const statusPill = document.getElementById('globalStatusPill'); if (!statusPill) return; statusPill.classList.remove('alarm-active', 'alarm-acknowledged'); if (alarmActive && !alarmAcknowledged) { statusPill.classList.add('alarm-active'); } else if (alarmAcknowledged) { statusPill.classList.add('alarm-acknowledged'); } } // Acknowledge alarm - called when user clicks the status button async function acknowledgeAlarm() { const statusPill = document.getElementById('globalStatusPill'); // Only allow acknowledgment if alarm is active if (!alarmActive && !statusPill.classList.contains('warn') && !statusPill.classList.contains('crit')) { console.log('No active alarm to acknowledge'); return; } try { const response = await fetch('/api/alarm/acknowledge', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); console.log('Alarm acknowledged:', data); alarmAcknowledged = true; alarmActive = false; updateAlarmVisualState(); logEvent('info', 'Alarm acknowledged - buzzer muted'); } else { console.error('Failed to acknowledge alarm:', response.statusText); logEvent('warn', 'Failed to acknowledge alarm'); } } catch (error) { console.error('Error acknowledging alarm:', error); logEvent('warn', 'Error acknowledging alarm: ' + error.message); } } // Fetch alarm status from server async function fetchAlarmStatus() { try { const response = await fetch('/api/alarm/status'); if (response.ok) { const data = await response.json(); alarmActive = data.alarm_active; alarmAcknowledged = data.alarm_acknowledged; updateAlarmVisualState(); } } catch (error) { console.error('Error fetching alarm status:', error); } } // Update alert banner function updateAlertBanner(data) { const banner = document.getElementById('alert-banner'); const alertText = document.getElementById('alertText'); const alertDistance = document.getElementById('alert-distance-value'); const alertIndicator = document.getElementById('alertIndicator'); if (!banner) return; banner.classList.add('hidden'); banner.classList.remove('alert-critical', 'alert-warning'); if (data.has_alert && !data.is_valid && data.max_distance_km !== null) { // Show distance alert banner.classList.remove('hidden'); banner.classList.add('alert-critical'); if (alertDistance) { alertDistance.textContent = `${data.max_distance_km.toFixed(1)} km`; } if (alertText) { alertText.textContent = `GPS Jamming or Spoofing Alert! Location Distance: ${data.max_distance_km.toFixed(1)} km`; } if (alertIndicator) { alertIndicator.style.background = 'var(--accent-red)'; } } else if (data.has_alert) { // Has alert but not distance-based (missing sources) banner.classList.remove('hidden'); banner.classList.add('alert-warning'); if (alertText) { alertText.textContent = 'Some GPS sources are missing or stale.'; } if (alertIndicator) { alertIndicator.style.background = 'var(--accent-amber)'; } } } // Helper function to show degraded state function showDegradedState(errorMessage) { lastFetchSucceeded = false; // Update status pill and border frame to degraded FIRST (before logging which could fail) try { const statusPill = document.getElementById('globalStatusPill'); const statusText = statusPill ? statusPill.querySelector('.status-text') : null; if (statusPill) { if (statusText) { statusText.textContent = 'GNSS Integrity: Degraded'; } else { statusPill.textContent = 'GNSS Integrity: Degraded'; } statusPill.classList.remove('crit'); statusPill.classList.add('warn'); // Activate alarm state if (!alarmAcknowledged) { alarmActive = true; updateAlarmVisualState(); } } const borderFrame = document.getElementById('healthBorderFrame'); if (borderFrame) { borderFrame.classList.remove('crit'); borderFrame.classList.add('warn'); } } catch (e) { console.error('Error updating status pill:', e); } // Mark all "Updated" timestamps as stale/error (red) try { const sources = ['nmea_primary', 'nmea_secondary', 'tm_ais', 'starlink_gps', 'starlink_location']; sources.forEach(sourceName => { const updateEl = document.getElementById(`update-${sourceName}`); if (updateEl) { updateEl.classList.add('stale-text'); } }); } catch (e) { console.error('Error updating source timestamps:', e); } // Log the error message last logEvent('crit', errorMessage); } // Fetch and update data async function fetchData() { try { const response = await fetch('/api/status'); if (!response.ok) { console.error('Failed to fetch status:', response.statusText); showDegradedState(`Server error: ${response.status} ${response.statusText}`); return; } const data = await response.json(); currentData = data; lastFetchSucceeded = true; // Update all sources const sources = ['nmea_primary', 'nmea_secondary', 'tm_ais', 'starlink_gps', 'starlink_location']; sources.forEach(sourceName => { updateSource(sourceName, data.sources[sourceName]); }); // If distance-based alert (GPS jamming/spoofing), mark ALL enabled sources as "AT RISK" if (data.has_alert && !data.is_valid && data.max_distance_km !== null) { sources.forEach(sourceName => { const sourceData = data.sources[sourceName]; // Only mark sources that have coordinates (participated in distance validation) if (sourceData && sourceData.enabled && sourceData.status !== 'missing' && sourceData.coordinates) { const card = document.getElementById(`card-${sourceName}`); const badge = document.getElementById(`badge-${sourceName}`); if (card) { card.classList.remove('ok', 'stale'); card.classList.add('crit'); } if (badge) { badge.className = 'badge badge-danger'; badge.textContent = 'AT RISK'; } } }); } // Capture initial load flag before updateMap modifies it const wasInitialLoad = isInitialMapLoad; // Update map updateMap(data); // Update global status updateGlobalStatus(data); // Update alert banner updateAlertBanner(data); // Load route data if enabled (fit bounds on initial load) if (window.SHOW_ROUTE && showRouteEnabled) { loadRouteData(wasInitialLoad); } } catch (error) { console.error('Error fetching data:', error); showDegradedState('Failed to fetch status data: ' + error.message); } } // Update relative times function updateRelativeTimes() { if (!currentData) return; const sources = ['nmea_primary', 'nmea_secondary', 'tm_ais', 'starlink_gps', 'starlink_location']; sources.forEach(sourceName => { const sourceData = currentData.sources[sourceName]; const updateEl = document.getElementById(`update-${sourceName}`); if (updateEl && sourceData && sourceData.enabled && sourceData.last_update_unix) { updateEl.textContent = formatRelativeTime(sourceData.last_update_unix); } }); } // Tab switching for mobile function initTabs() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { const tabName = btn.dataset.tab; // Update button states tabBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Update content visibility tabContents.forEach(content => { content.classList.remove('active'); if (content.id === `tab-${tabName}`) { content.classList.add('active'); } }); // Invalidate map size when switching to map tab if (tabName === 'map' && map) { setTimeout(() => { map.invalidateSize(); }, 100); } }); }); } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { // Initialize tabs initTabs(); // Initialize map initMap(); // Initialize route checkbox from stored preference initRouteCheckbox(); // Initial log logEvent('info', 'TM GNSS Guard dashboard initialized.'); // Initial data fetch fetchData(); // Initial alarm status fetch fetchAlarmStatus(); // Update relative times every second setInterval(updateRelativeTimes, 1000); // Fetch data every 10 seconds setInterval(fetchData, 10000); // Fetch alarm status every 5 seconds (more frequent to stay in sync with buzzer) setInterval(fetchAlarmStatus, 5000); // Log data fetch events (only when data actually changed) setInterval(() => { if (currentData && lastFetchSucceeded) { // Only log if validation timestamp changed (new data from backend) if (currentData.validation_timestamp !== lastValidationTimestamp) { lastValidationTimestamp = currentData.validation_timestamp; if (currentData.has_alert && !currentData.is_valid && currentData.max_distance_km !== null) { logEvent('crit', `Server reports alert: distance ${currentData.max_distance_km.toFixed(1)} km`); } else if (!currentData.is_valid) { logEvent('warn', 'Server reports validation issue.'); } else { logEvent('info', 'Server status OK.'); } } } }, 10000); }); // ============================================================================= // ROUTE VISUALIZATION (24h history) // ============================================================================= async function loadRouteData(fitBoundsAfter = false) { if (!window.SHOW_ROUTE) return; try { const response = await fetch('/api/route'); if (!response.ok) { // If route fetch fails on initial load, still fit bounds to source markers if (fitBoundsAfter) { fitBoundsToMarkers(); isInitialMapLoad = false; } return; } const routeData = await response.json(); renderRoute(routeData); // Fit bounds to include route on initial load if (fitBoundsAfter) { // Collect current source marker coordinates const sourceCoords = []; Object.values(markers).forEach(marker => { const latlng = marker.getLatLng(); sourceCoords.push([latlng.lat, latlng.lng]); }); // Use all route points (full 24h) for bounds calculation const routePointCoords = []; routeData.forEach(point => { if (point.lat && point.lng) { routePointCoords.push([point.lat, point.lng]); } }); const allCoords = [...sourceCoords, ...routePointCoords]; if (allCoords.length > 0) { const bounds = L.latLngBounds(allCoords); map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); } else { fitBoundsToMarkers(); } isInitialMapLoad = false; } } catch (error) { console.error('Error loading route:', error); // If route fetch fails on initial load, still fit bounds to source markers if (fitBoundsAfter) { fitBoundsToMarkers(); isInitialMapLoad = false; } } } function fitBoundsToMarkers() { const sourceCoords = []; Object.values(markers).forEach(marker => { const latlng = marker.getLatLng(); sourceCoords.push([latlng.lat, latlng.lng]); }); if (sourceCoords.length > 0) { const bounds = L.latLngBounds(sourceCoords); map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); } else if (currentData && currentData.map_center) { map.setView([currentData.map_center.latitude, currentData.map_center.longitude], 13); } } function renderRoute(routeData) { clearRouteMarkers(); routeCoords = []; // Reset route coordinates if (!showRouteEnabled || !routeData || routeData.length === 0) return; // Create small circle markers for route points routeData.forEach(point => { if (!point.lat || !point.lng) return; // Store coordinates for bounds calculation routeCoords.push([point.lat, point.lng]); // Determine color based on status let color; switch (point.status) { case 'alert': color = '#c62828'; // Red break; case 'degraded': color = '#ffa726'; // Amber break; default: color = '#1fad3a'; // Green } const marker = L.circleMarker([point.lat, point.lng], { radius: 4, fillColor: color, color: color, weight: 1, opacity: 0.8, fillOpacity: 0.6 }).addTo(map); // Add popup with details const popupContent = `
${point.sources_missing?.length ? `` : ''} ${point.sources_stale?.length ? `` : ''} ${point.max_distance_m > point.threshold_m ? `` : ''}
`; marker.bindPopup(popupContent); routeMarkers.push(marker); }); } function clearRouteMarkers() { routeMarkers.forEach(marker => { map.removeLayer(marker); }); routeMarkers = []; routeCoords = []; } function toggleRoute() { const checkbox = document.getElementById('showRoute'); showRouteEnabled = checkbox ? checkbox.checked : false; // Persist the preference saveRoutePreference(showRouteEnabled); if (showRouteEnabled) { loadRouteData(); } else { clearRouteMarkers(); } } // Initialize route checkbox state from localStorage function initRouteCheckbox() { const checkbox = document.getElementById('showRoute'); if (checkbox) { checkbox.checked = showRouteEnabled; } }