Spaces:
Running
Running
Download script.js from netomx/mapvoyage-compass: direct link, hf CLI and curl.
- Browser
- Download file 19.5 kB
-
https://huggingface.co/spaces/netomx/mapvoyage-compass/resolve/main/script.js
- Command line
-
hf download hf://spaces/netomx/mapvoyage-compass/script.js
-
curl -L -o script.js https://huggingface.co/spaces/netomx/mapvoyage-compass/resolve/main/script.js
19.5 kB
| // 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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', | |
| 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('<b>Welcome to MapVoyage Compass! 🧭</b><br>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: <br>${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: ` | |
| <div style=" | |
| background: ${color}; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| border: 3px solid white; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.3); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: bold; | |
| font-size: 12px; | |
| "> | |
| <i data-feather="battery-charging" style="width: 12px; height: 12px;"></i> | |
| </div> | |
| `, | |
| 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 = ` | |
| <div style="min-width: 250px; font-family: system-ui, -apple-system, sans-serif;"> | |
| <div style="display: flex; align-items: center; margin-bottom: 10px;"> | |
| <div style="width: 40px; height: 40px; background: ${color}; border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 10px;"> | |
| <i data-feather="battery-charging" style="width: 20px; height: 20px; color: white;"></i> | |
| </div> | |
| <div> | |
| <h3 style="margin: 0; font-weight: 600; color: #1f2937;">Charge Point #${chargePoint.id}</h3> | |
| <p style="margin: 2px 0 0 0; font-size: 12px; color: #6b7280;">${chargePoint.vendor || 'Unknown Vendor'}</p> | |
| </div> | |
| </div> | |
| <div style="background: #f9fafb; border-radius: 8px; padding: 12px; margin-bottom: 10px;"> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 6px;"> | |
| <span style="font-size: 13px; color: #6b7280;">Model:</span> | |
| <span style="font-weight: 500; color: #374151;">${chargePoint.model || 'N/A'}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 6px;"> | |
| <span style="font-size: 13px; color: #6b7280;">Connectors:</span> | |
| <span style="font-weight: 500; color: #374151;">${chargePoint.num_connectors || 'N/A'}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 6px;"> | |
| <span style="font-size: 13px; color: #6b7280;">Serial:</span> | |
| <span style="font-weight: 500; color: #374151;">${chargePoint.serial_number || 'N/A'}</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <span style="font-size: 13px; color: #6b7280;">Last Heartbeat:</span> | |
| <span style="font-weight: 500; color: #374151;">${chargePoint.last_heartbeat ? new Date(chargePoint.last_heartbeat).toLocaleString() : 'Never'}</span> | |
| </div> | |
| </div> | |
| ${chargePoint.address ? ` | |
| <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #e5e7eb;"> | |
| <p style="margin: 0; font-size: 13px; color: #6b7280;">Address:</p> | |
| <p style="margin: 4px 0 0 0; font-size: 14px; color: #374151;">${chargePoint.address}</p> | |
| </div> | |
| ` : ''} | |
| <div style="margin-top: 12px; display: flex; justify-content: space-between;"> | |
| <button onclick="navigateToPoint(${lat}, ${lng})" style=" | |
| background: #3b82f6; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| "> | |
| <i data-feather="navigation" style="width: 12px; height: 12px;"></i> | |
| Navigate | |
| </button> | |
| <button onclick="showDetails(${chargePoint.id})" style=" | |
| background: #f3f4f6; | |
| color: #374151; | |
| border: 1px solid #d1d5db; | |
| border-radius: 6px; | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| "> | |
| <i data-feather="info" style="width: 12px; height: 12px;"></i> | |
| Details | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| 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 = ` | |
| <div class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-bold text-gray-800">Charge Point #${pointId} Details</h3> | |
| <button id="closeModal" class="text-gray-400 hover:text-gray-600"> | |
| <i data-feather="x" class="w-6 h-6"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div class="bg-blue-50 p-4 rounded-xl"> | |
| <div class="flex items-center"> | |
| <i data-feather="info" class="w-5 h-5 text-blue-600 mr-2"></i> | |
| <p class="text-blue-800">Detailed information would be fetched from the API for point ID ${pointId}.</p> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 class="font-medium text-gray-700 mb-2">Advanced Actions</h4> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <button class="bg-blue-100 text-blue-700 hover:bg-blue-200 py-2 px-4 rounded-lg flex items-center justify-center gap-2"> | |
| <i data-feather="refresh-cw" class="w-4 h-4"></i> | |
| Refresh Status | |
| </button> | |
| <button class="bg-green-100 text-green-700 hover:bg-green-200 py-2 px-4 rounded-lg flex items-center justify-center gap-2"> | |
| <i data-feather="zap" class="w-4 h-4"></i> | |
| Start Charging | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-end"> | |
| <button id="closeModalBtn" class="bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-2 px-6 rounded-lg"> | |
| Close | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| 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 = '<i data-feather="refresh-cw" class="w-6 h-6 text-blue-600"></i>'; | |
| 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 = ` | |
| <div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> | |
| <span>Auto‑refresh every 10s</span> | |
| `; | |
| 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 = ` | |
| <div class="flex items-center"> | |
| <div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-3"> | |
| <i data-feather="battery-charging" class="text-blue-600"></i> | |
| </div> | |
| <div> | |
| <p class="font-semibold text-gray-800">Charge Points</p> | |
| <p class="text-gray-600 text-sm">${message}</p> | |
| </div> | |
| </div> | |
| `; | |
| // 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); | |
| } | |