/** * rechercher les bornes de recharge, * afficher des cercles colorés selon la puissance max de la station * lister les bornes trouvées dans la page * @type {boolean} */ import {lcm_i18n} from './lcm_i18n.js'; // Détecter la langue du navigateur const currentLanguage = lcm_i18n.detectLanguage(); console.log('Langue détectée:', currentLanguage); import lcm_config from './lcm_config.js' import lcm_utils, { valid_qa_message } from './lcm_utils.js' import lcm_color_utils from './lcm_color_utils.js' import { sendToJOSM, createJOSMEditLink } from './lcm_editor.js' import routing from './lcm_routing.js' let geojsondata; let lastLatLng; let searchLocationMarker = null; let count_hidden_by_filters = 0; let averageChargeKwh = 26; let routePolyline = null; let routeMarkers = []; let startMarker = null; let endMarker = null; let startCoords = null; let endCoords = null; // Déclarer les variables d'itinéraire au début let startItinerary = [0, 0]; let endItinerary = [0, 0]; // serveurs de tuiles: https://wiki.openstreetmap.org/wiki/Tile_servers // https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png // https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png // https://tile.openstreetmap.org/{z}/{x}/{y}.png // 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png' // Créer la carte centrée sur Rouen // Liste des 20 villes les plus peuplées de France avec leurs coordonnées géographiques // Initialisation de la carte avec la vue centrée sur la ville choisie let map = L.map('map') L.control.scale().addTo(map) setCoordinatesOfLeafletMapFromQueryParameters() /** * filtres à toggle par des boutons dans la page * à appliquer à chaque rafraîchissement des points geojson * TODO: make buttons and filter in refresh circles */ let filterStatesAvailable = ['hide', 'show', 'showOnly'] let display_unknown_max_power_station = 'show'; let display_alert_cable_missing = 'show'; const start = map.getCenter(); const startLat = start.lat; const startLon = start.lng; // Ajouter cette fonction avant searchLocation function moveToLocation(place) { const lat = parseFloat(place.lat); const lon = parseFloat(place.lon); if (isNaN(lat) || isNaN(lon)) { console.error('Coordonnées invalides:', place); return; } // Supprimer l'ancien marqueur s'il existe if (searchLocationMarker) { map.removeLayer(searchLocationMarker); } // Créer un nouveau marqueur avec une icône personnalisée searchLocationMarker = L.marker([lat, lon], { icon: L.divIcon({ className: 'search-location-marker', html: '📍', iconSize: [30, 30], iconAnchor: [15, 30] }) }); // Ajouter un popup avec le nom du lieu const popupContent = ` ${place.display_name} ${place.type ? `
Type: ${place.type}` : ''} ${place.context ? `
${place.context}` : ''} `; searchLocationMarker.bindPopup(popupContent); // Ajouter le marqueur à la carte searchLocationMarker.addTo(map); // Désactiver temporairement l'événement moveend map.off('moveend', onMapMoveEnd); // Centrer la carte sur le lieu map.setView([lat, lon], map.getZoom()); // Réactiver l'événement moveend après un court délai setTimeout(() => { map.on('moveend', onMapMoveEnd); }, 500); // Ouvrir le popup automatiquement searchLocationMarker.openPopup(); } // Déplacer searchLocationWithAddok avant searchLocation function searchLocationWithAddok(searchText, mapCenter) { const baseUrl = 'https://demo.addok.xyz/search'; const params = new URLSearchParams({ q: searchText, limit: 10, lat: mapCenter.lat, lon: mapCenter.lng }); const url = `${baseUrl}?${params.toString()}`; return fetch(url) .then(response => { if (!response.ok) { throw new Error('Erreur réseau lors de la recherche'); } return response.json(); }) .then(data => { if (!data.features || data.features.length === 0) { throw new Error('Aucun résultat trouvé'); } return data.features.map(feature => ({ lat: feature.geometry.coordinates[1], lon: feature.geometry.coordinates[0], display_name: feature.properties.label, importance: feature.properties.score, context: feature.properties.context, type: feature.properties.type, city: feature.properties.city, distance: feature.properties.distance })); }); } // Modifier la fonction searchLocation function searchLocation() { const location = $('#searchLocation').val(); if (!location) { alert('Veuillez entrer un lieu à rechercher.'); return; } const useAddok = $('#useAddok').is(':checked'); const searchPromise = useAddok ? searchLocationWithAddok(location, map.getCenter()) : fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(location)}`) .then(response => response.json()); searchPromise .then(data => { const resultsDropdown = $('#searchResults'); resultsDropdown.empty(); if (!data || data.length === 0) { alert('Lieu non trouvé. Veuillez essayer un autre terme de recherche.'); resultsDropdown.hide(); return; } // Toujours sélectionner le premier résultat moveToLocation(data[0]); displayPointsFromApi(); // Si il y a plus d'un résultat, les afficher quand même dans la liste if (data.length > 1) { // Vérifier si le bouton de fermeture existe déjà if ($('.close-results-button').length === 0) { const closeButton = $('
`) .openOn(map); }); // Fonctions globales pour le menu contextuel window.setStartMarkerFromPopup = function(latlng) { startItinerary = [latlng[0], latlng[1]] setStartMarker({lat: latlng[0], lng: latlng[1]}); map.closePopup(); if (endItinerary) calculerEtAfficherItineraire(); }; window.setEndMarkerFromPopup = function(latlng) { setEndMarker({lat: latlng[0], lng: latlng[1]}); endItinerary = [latlng[0], latlng[1]] map.closePopup(); if (startItinerary) calculerEtAfficherItineraire(); }; // Déclarer une variable globale pour le tracé window.routePolyline = null; window.lastRouteDestination = null; // Ajouter cette fonction avant calculerEtAfficherItineraire async function searchChargingStationsAroundPoint(lat, lon, radius = 1500) { const query = ` [out:json][timeout:25]; ( node["amenity"="charging_station"](around:${radius},${lat},${lon}); way["amenity"="charging_station"](around:${radius},${lat},${lon}); relation["amenity"="charging_station"](around:${radius},${lat},${lon}); ); out body; >; out skel qt;`; const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`; try { const response = await fetch(url); const data = await response.json(); return data.elements || []; } catch (error) { console.error('Erreur lors de la recherche des stations de recharge:', error); return []; } } async function calculerEtAfficherItineraire(destination, mode = 'car') { // Récupérer le point de départ (centre de la carte) const startLat = startItinerary[0]; const startLon = startItinerary[1]; const endLat = endItinerary[0]; const endLon = endItinerary[1]; // Récupérer les paramètres de la batterie const batteryCapacity = parseFloat($('#battery_capacity').val()) || 40; const batteryStartLevel = parseFloat($('#battery_start_level').val()) || 90; const consumptionPerKm = parseFloat($('#consumption_per_km').val()) || 160; const minBatteryLevel = parseFloat($('#min_battery_level').val()) || 15; const chargeToLevel = parseFloat($('#charge_to_level').val()) || 80; // Construire l'URL OSRM const url = `https://router.project-osrm.org/route/v1/${mode}/${startLon},${startLat};${endLon},${endLat}?overview=full&geometries=geojson&alternatives=false&steps=false`; $('#current_station_infos').html('Calcul de l\'itinéraire en cours…'); try { const response = await fetch(url); const data = await response.json(); if (!data.routes || !data.routes.length) { $('#current_station_infos').html('Aucun itinéraire trouvé.'); return; } const route = data.routes[0]; const coords = route.geometry.coordinates.map(c => [c[1], c[0]]); // [lat, lon] // Supprimer l'ancien tracé et les marqueurs if (window.routePolyline) { map.removeLayer(window.routePolyline); } if (window.pauseMarkers) { window.pauseMarkers.forEach(marker => map.removeLayer(marker)); } if (window.batteryMarkers) { window.batteryMarkers.forEach(marker => map.removeLayer(marker)); } // Afficher le nouveau tracé sur la carte window.routePolyline = L.polyline(coords, {color: '#0059ce', weight: 5}).addTo(map); // Supprimer le fitBounds qui déplace la vue // map.fitBounds(window.routePolyline.getBounds()); // Calculer la consommation totale const totalDistanceKm = route.distance / 1000; const totalConsumptionKwh = (totalDistanceKm * consumptionPerKm) / 1000; const batteryStartKwh = (batteryCapacity * batteryStartLevel) / 100; const batteryEndKwh = batteryStartKwh - totalConsumptionKwh; const batteryEndLevel = (batteryEndKwh / batteryCapacity) * 100; // Ajouter les marqueurs de niveau de batterie window.batteryMarkers = []; const batteryLevels = [90, 80, 70, 60, 50, 40, 30, 20, 15]; let accumulatedDistance = 0; let currentBatteryLevel = batteryStartLevel; for (let i = 0; i < coords.length - 1; i++) { const segmentDistance = L.latLng(coords[i]).distanceTo(coords[i + 1]) / 1000; // en km const segmentConsumption = (segmentDistance * consumptionPerKm) / 1000; // en kWh const nextBatteryLevel = ((batteryStartKwh - (accumulatedDistance * consumptionPerKm / 1000)) / batteryCapacity) * 100; // Vérifier si on atteint un niveau de batterie à marquer for (const level of batteryLevels) { if (currentBatteryLevel > level && nextBatteryLevel <= level) { const ratio = (level - currentBatteryLevel) / (nextBatteryLevel - currentBatteryLevel); const markerCoords = [ coords[i][0] + (coords[i + 1][0] - coords[i][0]) * ratio, coords[i][1] + (coords[i + 1][1] - coords[i][1]) * ratio ]; const batteryMarker = L.marker(markerCoords, { icon: L.divIcon({ className: 'battery-marker', html: `🔋 ${level}%`, iconSize: [30, 30] }) }).addTo(map); let popupContent = `

Niveau de batterie : ${level}%

`; if (level <= minBatteryLevel) { popupContent += `

⚠️ Recharge recommandée !

`; } popupContent += `

Distance parcourue : ${(accumulatedDistance + segmentDistance * ratio).toFixed(1)} km

Consommation : ${((accumulatedDistance + segmentDistance * ratio) * consumptionPerKm / 1000).toFixed(1)} kWh

`; batteryMarker.bindPopup(popupContent); window.batteryMarkers.push(batteryMarker); } } accumulatedDistance += segmentDistance; currentBatteryLevel = nextBatteryLevel; } // Calculer le nombre de pauses nécessaires (1 pause toutes les 2 heures) const durationHours = route.duration / 3600; const numberOfPauses = Math.floor(durationHours / 2); const pauseDurationMinutes = 10; const totalPauseTimeMinutes = numberOfPauses * pauseDurationMinutes; // Ajouter les marqueurs de pause window.pauseMarkers = []; if (numberOfPauses > 0) { const totalDistance = route.distance; const pauseInterval = totalDistance / (numberOfPauses + 1); for (let i = 1; i <= numberOfPauses; i++) { const pauseDistance = pauseInterval * i; let accumulatedDistance = 0; let pauseCoords; // Trouver les coordonnées pour cette pause for (let j = 0; j < coords.length - 1; j++) { const segmentDistance = L.latLng(coords[j]).distanceTo(coords[j + 1]); if (accumulatedDistance + segmentDistance >= pauseDistance) { const ratio = (pauseDistance - accumulatedDistance) / segmentDistance; pauseCoords = [ coords[j][0] + (coords[j + 1][0] - coords[j][0]) * ratio, coords[j][1] + (coords[j + 1][1] - coords[j][1]) * ratio ]; break; } accumulatedDistance += segmentDistance; } if (pauseCoords) { // Calculer la distance restante jusqu'à la prochaine pause ou la fin const nextPauseDistance = (i < numberOfPauses) ? pauseInterval : (totalDistance - pauseDistance); const consumptionToNextPause = (nextPauseDistance / 1000) * consumptionPerKm / 1000; // en kWh const batteryLevelNeeded = (consumptionToNextPause / batteryCapacity) * 100 + minBatteryLevel; // Calculer le niveau de batterie actuel à ce point const currentBatteryLevel = ((batteryStartKwh - (pauseDistance / 1000 * consumptionPerKm / 1000)) / batteryCapacity) * 100; // Rechercher les stations de recharge autour du point de pause const chargingStations = await searchChargingStationsAroundPoint(pauseCoords[0], pauseCoords[1]); // Créer le contenu du popup avec les stations trouvées let popupContent = `

Pause recommandée n°${i}

Durée : 10 minutes

Niveau de batterie actuel : ${currentBatteryLevel.toFixed(1)}%

Niveau de batterie nécessaire : ${batteryLevelNeeded.toFixed(1)}%

Distance jusqu'à la prochaine pause : ${(nextPauseDistance / 1000).toFixed(1)} km

`; if (chargingStations.length > 0) { popupContent += `

Stations de recharge à proximité (${chargingStations.length}) :

`; } else { popupContent += `

Aucune station de recharge trouvée dans un rayon de 1.5 km

`; } popupContent += `
`; const pauseMarker = L.marker(pauseCoords, { icon: L.divIcon({ className: 'pause-marker', html: '⏸️', iconSize: [30, 30] }) }).addTo(map); pauseMarker.bindPopup(popupContent); window.pauseMarkers.push(pauseMarker); } } } // Afficher distance, durée, et boutons de mode de transport const distanceKm = (route.distance / 1000).toFixed(1); const durationMin = Math.round(route.duration / 60); const totalDurationMin = durationMin + totalPauseTimeMinutes; // Convertir la durée totale en heures et minutes si nécessaire let durationDisplay = ''; if (totalDurationMin >= 60) { const hours = Math.floor(totalDurationMin / 60); const minutes = totalDurationMin % 60; durationDisplay = `${hours}h${minutes > 0 ? ` ${minutes}min` : ''}`; } else { durationDisplay = `${totalDurationMin} min`; } $('#current_station_infos').html(`

Itinéraire

Distance : ${distanceKm} km
Durée estimée : ${durationDisplay} (dont ${totalPauseTimeMinutes} min de pause)
Nombre de pauses recommandées : ${numberOfPauses} (10 min toutes les 2h)
Niveau de batterie final estimé : ${batteryEndLevel.toFixed(1)}%
Consommation totale estimée : ${totalConsumptionKwh.toFixed(1)} kWh
`); // Stocker la destination pour recalculer facilement window.lastRouteDestination = destination; } catch (e) { $('#current_station_infos').html('Erreur lors du calcul de l\'itinéraire.'); } } function setStartMarker(latlng) { if (startMarker) map.removeLayer(startMarker); startMarker = L.marker(latlng, { draggable: true, icon: L.divIcon({className: 'start-marker', html: '🟢', iconSize: [30, 30]}) }).addTo(map); startCoords = [latlng.lat, latlng.lng]; startMarker.on('dragend', function(e) { startCoords = [e.target.getLatLng().lat, e.target.getLatLng().lng]; if (endCoords) calculerEtAfficherItineraire(); }); } function setEndMarker(latlng) { if (endMarker) map.removeLayer(endMarker); endMarker = L.marker(latlng, { draggable: true, icon: L.divIcon({className: 'end-marker', html: '🔴', iconSize: [30, 30]}) }).addTo(map); endCoords = [latlng.lat, latlng.lng]; endMarker.on('dragend', function(e) { endCoords = [e.target.getLatLng().lat, e.target.getLatLng().lng]; if (startCoords) calculerEtAfficherItineraire(); }); }