// Main JavaScript for MapVoyage Compass // API Configuration const API_BASE_URL = 'https://ocpp.neto.mx:8082/api'; const ACCESS_TOKEN = '1bc2b30bcde5c4a31a1e6d6e8637e149f16102f43c46db22ca19ddd1348cb745'; // Global variables let map; let chargePointMarkers = []; let chargePointLayerGroup; let refreshInterval; // Initialize OpenStreetMap with Positron style function initMap() { // Default coordinates (centered on Mexico) const defaultLat = 23.6345; const defaultLng = -102.5528; const defaultZoom = 5; // Create map instance with zoom control disabled map = L.map('map', { zoomControl: false }).setView([defaultLat, defaultLng], defaultZoom); // Add Positron tile layer (CartoDB Positron) L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 19, }).addTo(map); // Create a layer group for charge points chargePointLayerGroup = L.layerGroup().addTo(map); // Add a marker at the center const marker = L.marker([defaultLat, defaultLng]).addTo(map); marker.bindPopup('Welcome to MapVoyage Compass! đź§­
Now showing electric vehicle charge points.').openPopup(); // Add click event to get coordinates map.on('click', function(e) { const popup = L.popup() .setLatLng(e.latlng) .setContent(`You clicked at:
${e.latlng.lat.toFixed(4)}, ${e.latlng.lng.toFixed(4)}`) .openOn(map); }); // Load charge points from API loadChargePoints(); // Start auto‑refresh every 10 seconds startAutoRefresh(); // Store map instance globally for other functions window.map = map; } // Load charge points from API async function loadChargePoints() { try { // Silent load: no notification const response = await fetch(`${API_BASE_URL}/charge_points`, { method: 'GET', headers: { 'Authorization': `Bearer ${ACCESS_TOKEN}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } const chargePoints = await response.json(); // Filter points with valid coordinates const validPoints = chargePoints.filter(point => point.latitude && point.longitude && point.latitude !== 'null' && point.longitude !== 'null' && !isNaN(parseFloat(point.latitude)) && !isNaN(parseFloat(point.longitude)) ); if (validPoints.length === 0) { // No valid points, but do nothing silently return; } // Create a lookup of existing markers by IDs (or coordinates) to update/remove // For simplicity, we'll clear and re‑add, but keep the map view. // First, store current map view to preserve it. const currentCenter = map.getCenter(); const currentZoom = map.getZoom(); // Clear existing markers chargePointMarkers.forEach(marker => marker.remove()); chargePointMarkers = []; chargePointLayerGroup.clearLayers(); // Create markers for each charge point validPoints.forEach(point => { const marker = createChargePointMarker(point); chargePointMarkers.push(marker); chargePointLayerGroup.addLayer(marker); }); // Restore map view (no recentering) map.setView(currentCenter, currentZoom); // No success notification } catch (error) { console.error('Error loading charge points:', error); // Silent fallback to demo data loadDemoChargePoints(); } } // Start auto‑refresh every 10 seconds function startAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); } refreshInterval = setInterval(() => { loadChargePoints(); console.log('Auto‑refresh executed at', new Date().toLocaleTimeString()); }, 10000); // 10 seconds } // Stop auto‑refresh (useful for manual control) function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; showNotification('Auto‑refresh stopped'); } } // Toggle auto‑refresh on/off function toggleAutoRefresh() { if (refreshInterval) { stopAutoRefresh(); } else { startAutoRefresh(); } } // Create a marker for a charge point function createChargePointMarker(chargePoint) { // Determine color based on status let color = '#3b82f6'; // Default blue let fillColor = '#8b5cf6'; if (chargePoint.last_heartbeat) { const lastHeartbeat = new Date(chargePoint.last_heartbeat); const now = new Date(); const hoursDiff = (now - lastHeartbeat) / (1000 * 60 * 60); if (hoursDiff > 24) { color = '#ef4444'; // Red for offline (>24h) fillColor = '#f87171'; } else if (hoursDiff > 1) { color = '#f59e0b'; // Yellow for stale (1-24h) fillColor = '#fbbf24'; } else { color = '#10b981'; // Green for recent (<1h) fillColor = '#34d399'; } } // Create custom icon const icon = L.divIcon({ html: `
`, className: 'charge-point-marker', iconSize: [24, 24], iconAnchor: [12, 12] }); const lat = parseFloat(chargePoint.latitude); const lng = parseFloat(chargePoint.longitude); const marker = L.marker([lat, lng], { icon: icon }); // Create popup content const popupContent = `

Charge Point #${chargePoint.id}

${chargePoint.vendor || 'Unknown Vendor'}

Model: ${chargePoint.model || 'N/A'}
Connectors: ${chargePoint.num_connectors || 'N/A'}
Serial: ${chargePoint.serial_number || 'N/A'}
Last Heartbeat: ${chargePoint.last_heartbeat ? new Date(chargePoint.last_heartbeat).toLocaleString() : 'Never'}
${chargePoint.address ? `

Address:

${chargePoint.address}

` : ''}
`; marker.bindPopup(popupContent); // Initialize feather icons in popup when it opens marker.on('popupopen', () => { feather.replace(); }); return marker; } // Load demo charge points if API fails function loadDemoChargePoints() { const demoPoints = [ { id: 1, address: "Sta. Barbara 1993, Jardines de la Silla, 67287 Jardines de la Silla, N.L.", latitude: "25.632163", longitude: "-100.178570", model: "AC-Pro", vendor: "DELTRIX", num_connectors: 1, serial_number: "gbt001", last_heartbeat: "2025-12-28 07:41:21" }, { id: 2, address: "Sta. Barbara 1993, Jardines de la Silla, 67287 Jardines de la Silla, N.L.", latitude: "25.631998", longitude: "-100.179111", model: "CCS2+GBT", vendor: "", num_connectors: 2, serial_number: "1993", last_heartbeat: "2025-12-28 07:41:21" }, { id: 3, address: "Santa Barbara 1983, Jardines de la Silla, Juarez, N.L.", latitude: "25.632263", longitude: "-100.178152", model: "", vendor: "charger Point", num_connectors: 1, serial_number: "1985", last_heartbeat: "2025-12-28 07:41:22" } ]; // Store current view to preserve it const currentCenter = map.getCenter(); const currentZoom = map.getZoom(); demoPoints.forEach(point => { const marker = createChargePointMarker(point); chargePointMarkers.push(marker); chargePointLayerGroup.addLayer(marker); }); // Keep the same map view (no recentering) map.setView(currentCenter, currentZoom); } // Navigate to a point (center map on it) function navigateToPoint(lat, lng) { map.setView([lat, lng], 15); // Open popup if marker exists chargePointMarkers.forEach(marker => { const markerLatLng = marker.getLatLng(); if (markerLatLng.lat === lat && markerLatLng.lng === lng) { marker.openPopup(); } }); showNotification(`Navigating to charge point at ${lat.toFixed(4)}, ${lng.toFixed(4)}`); } // Show details for a charge point function showDetails(pointId) { // In a real app, this would fetch more details from the API showNotification(`Loading details for charge point #${pointId}...`); // Simulate API call setTimeout(() => { const modal = document.createElement('div'); modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4'; modal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; modal.innerHTML = `

Charge Point #${pointId} Details

Detailed information would be fetched from the API for point ID ${pointId}.

Advanced Actions

`; document.body.appendChild(modal); feather.replace(); const closeModal = () => { document.body.removeChild(modal); }; modal.querySelector('#closeModal').addEventListener('click', closeModal); modal.querySelector('#closeModalBtn').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); }, 500); } // Refresh charge points function refreshChargePoints() { // Silent refresh: no notification loadChargePoints(); } // Add refresh button to the UI function addRefreshButton() { const refreshBtn = document.createElement('button'); refreshBtn.id = 'refreshChargePointsBtn'; refreshBtn.className = 'fixed bottom-24 right-6 z-10 bg-white rounded-full p-3 shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center'; refreshBtn.innerHTML = ''; refreshBtn.title = 'Refresh Charge Points'; refreshBtn.style.width = '56px'; refreshBtn.style.height = '56px'; refreshBtn.addEventListener('click', refreshChargePoints); document.body.appendChild(refreshBtn); feather.replace(); } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', function() { initMap(); addRefreshButton(); // Add refresh button to user menu component document.addEventListener('userMenuReady', function() { const userMenu = document.querySelector('user-circle-menu'); if (userMenu && userMenu.shadowRoot) { // We'll add a refresh option to the user menu via event setTimeout(() => { // Add global refresh function window.refreshChargePoints = refreshChargePoints; // Also expose auto‑refresh controls window.startAutoRefresh = startAutoRefresh; window.stopAutoRefresh = stopAutoRefresh; window.toggleAutoRefresh = toggleAutoRefresh; }, 100); } }); // Auto‑refresh indicator const indicator = document.createElement('div'); indicator.id = 'autoRefreshIndicator'; indicator.className = 'fixed bottom-6 right-6 z-10 bg-green-100 text-green-800 text-xs font-medium px-3 py-2 rounded-full shadow flex items-center space-x-2'; indicator.innerHTML = `
Auto‑refresh every 10s `; document.body.appendChild(indicator); }); // Show notification function function showNotification(message) { // Create notification element const notification = document.createElement('div'); notification.className = 'fixed top-6 right-6 z-50 bg-white rounded-xl shadow-2xl p-4 border-l-4 border-blue-500 transform transition-all duration-300 translate-x-full'; notification.innerHTML = `

Charge Points

${message}

`; // Add to DOM document.body.appendChild(notification); // Trigger feather icons replacement feather.replace(); // Animate in setTimeout(() => { notification.classList.remove('translate-x-full'); notification.classList.add('translate-x-0'); }, 10); // Auto remove after 3 seconds setTimeout(() => { notification.classList.remove('translate-x-0'); notification.classList.add('translate-x-full'); setTimeout(() => { document.body.removeChild(notification); }, 300); }, 3000); }