mapvoyage-compass / script.js
netomx's picture
Do not recenter map every refresh. Do not show notification of refreshing, nor Loading.
b21f67e verified
Raw History Blame Contribute Delete
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <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);
}