libre-charge-map/js/lcm_main.js
2025-05-04 23:48:53 +02:00

1905 lines
No EOL
71 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 = `
<strong>${place.display_name}</strong>
${place.type ? `<br>Type: ${place.type}` : ''}
${place.context ? `<br>${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 = $('<button>')
.addClass('close-results-button')
.html('❌')
.attr('title', 'Fermer les résultats de recherche')
.on('click', function () {
$('#searchResults').hide();
$(this).hide();
$('#searchLocation').val('').focus();
});
resultsDropdown.before(closeButton);
}
data.forEach((place, index) => {
let displayText = place.display_name;
if (useAddok && place.distance) {
displayText += ` (${Math.round(place.distance)}m)`;
}
const option = $('<option></option>')
.val(index)
.text(displayText)
.data('place', place);
// Création du bouton itinéraire
const routeBtn = $('<button>')
.addClass('route-to-place')
.text('Itinéraire '+index)
.on('click', function(e) {
e.preventDefault();
calculerEtAfficherItineraire(place, 'car');
});
// Ajoute le bouton à côté de l'option (ou dans une div, selon ton HTML)
resultsDropdown.append(option);
resultsDropdown.after(routeBtn);
});
resultsDropdown.show();
$('.close-results-button').show();
// Sélectionner visuellement le premier résultat dans la liste
resultsDropdown.val(0);
} else {
resultsDropdown.hide();
}
})
.catch(error => {
console.error('Erreur lors de la recherche du lieu :', error);
alert('Erreur lors de la recherche du lieu : ' + error.message);
});
}
function setRandomView() {
let randomCity = lcm_utils.cities[Math.floor(Math.random() * lcm_utils.cities.length)];
map = map.setView(randomCity.coords, lcm_config.initialZoom);
}
function setCoordinatesOfLeafletMapFromQueryParameters() {
const urlParams = new URLSearchParams(window.location.href);
const lat = urlParams.get('lat');
const lng = urlParams.get('lng');
const zoom = urlParams.get('zoom');
const startLat = urlParams.get('startLat');
const startLng = urlParams.get('startLng');
const endLat = urlParams.get('endLat');
const endLng = urlParams.get('endLng');
const batteryCapacity = urlParams.get('batteryCapacity');
const consumptionPerKm = urlParams.get('consumptionPerKm');
// Mettre à jour les champs de batterie si présents dans l'URL
if (batteryCapacity) {
$('#battery_capacity').val(batteryCapacity);
}
if (consumptionPerKm) {
$('#consumption_per_km').val(consumptionPerKm);
}
// Mettre à jour les coordonnées de départ et d'arrivée si présentes
if (startLat && startLng) {
startItinerary = [parseFloat(startLat), parseFloat(startLng)];
}
if (endLat && endLng) {
endItinerary = [parseFloat(endLat), parseFloat(endLng)];
}
// Initialiser la carte avec les coordonnées par défaut
if (!map) {
map = L.map('map');
L.control.scale().addTo(map);
}
// Si des coordonnées sont fournies dans l'URL, les utiliser
if (lat && lng && zoom) {
map.setView([lat, lng], zoom);
} else {
console.error('Les paramètres de coordonnées et de zoom doivent être présents dans l\'URL.');
setRandomView();
}
calculerEtAfficherItineraire(endItinerary)
}
// mettre à jour les infos queryparam dans l'url pour passer le lien avec l'état inclus
function updateURLWithMapCoordinatesAndZoom() {
// Récupère les coordonnées et le niveau de zoom de la carte
const center = map.getCenter();
const zoom = map.getZoom();
const batteryCapacity = $('#battery_capacity').val();
const consumptionPerKm = $('#consumption_per_km').val();
// Construit l'URL avec tous les paramètres
const url = `#coords=1&lat=${center.lat}&lng=${center.lng}&zoom=${zoom}` +
`&startLat=${startItinerary[0]}&startLng=${startItinerary[1]}` +
`&endLat=${endItinerary[0]}&endLng=${endItinerary[1]}` +
`&batteryCapacity=${batteryCapacity}&consumptionPerKm=${consumptionPerKm}`;
history.replaceState(null, null, url);
updateExternalEditorsLinks();
}
let all_stations_markers = L.layerGroup().addTo(map) // layer group pour tous les marqueurs
let bad_tags_markers = L.layerGroup()// layer group pour les marqueurs avec des problèmes de qualité
// let stations_much_speed_wow = L.layerGroup().addTo(map) // layer group des stations rapides
var osm = L.tileLayer(lcm_config.tileServers.osm, {
attribution: lcm_config.osmMention + '&copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
})
var cycle = L.tileLayer(lcm_config.tileServers.cycle, {
attribution: lcm_config.osmMention + '&copy; <a href="https://www.opencyclemap.org/">OpenCycleMap</a> contributors'
})
var transport = L.tileLayer(lcm_config.tileServers.transport, {
attribution: lcm_config.osmMention
})
let tileGrey =
L.tileLayer(lcm_config.tileServers.cartodb, {
attribution: lcm_config.osmMention
})
let stamen =
L.tileLayer(lcm_config.tileServers.stamen, {
attribution: lcm_config.osmMention
})
// Ajouter après les autres déclarations de tileLayer
let bdortho = L.tileLayer('https://wxs.ign.fr/ortho/geoportail/wmts?' +
'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTHOIMAGERY.ORTHOPHOTOS' +
'&STYLE=normal&FORMAT=image/jpeg&TILEMATRIXSET=PM&' +
'TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', {
attribution: '&copy; <a href="https://geoservices.ign.fr/bdortho">IGN-F/Géoportail</a>',
maxZoom: 19
});
// Modifier la définition de baseLayers pour inclure la BD ORTHO
var baseLayers = {
'Grey': tileGrey,
// 'Stamen': stamen,
'OpenStreetMap': osm,
// 'BD ORTHO IGN': bdortho,
// 'OpenCycleMap': cycle,
'Transport': transport
}
tileGrey.addTo(map)
function buildOverpassApiUrl(map, overpassQuery) {
let baseUrl = 'https://overpass-api.de/api/interpreter';
const kilometersMarginForLoading = 2;
const marginInDegrees = kilometersMarginForLoading / 111;
const south = map.getBounds().getSouth() - marginInDegrees;
const west = map.getBounds().getWest() - marginInDegrees;
const north = map.getBounds().getNorth() + marginInDegrees;
const east = map.getBounds().getEast() + marginInDegrees;
let bounds = south + ',' + west + ',' + north + ',' + east;
let resultUrl, query = '';
if (lcm_config.overrideQuery) {
query = `?data=[out:json][timeout:15];(
nwr[amenity=charging_station](${bounds});
);out body geom;`;
} else {
let nodeQuery = 'node[' + overpassQuery + '](' + bounds + ');';
let wayQuery = 'way[' + overpassQuery + '](' + bounds + ');';
let relationQuery = 'relation[' + overpassQuery + '](' + bounds + ');';
query = '?data=[out:json][timeout:15];(' + nodeQuery + wayQuery + relationQuery + ');out body geom;';
}
resultUrl = baseUrl + query;
return resultUrl;
}
function supprimerMarqueurs() {
all_stations_markers.clearLayers();
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
layer.remove();
}
});
}
let coef_reduction_bars = 0.8
function calculerPourcentage(partie, total, reduc) {
if (total === 0) {
return 'Division par zéro impossible'
}
let coef_reduction = 1
if (reduc) {
coef_reduction = coef_reduction_bars
}
return ((partie / total) * 100 * coef_reduction).toFixed(1)
}
// Ajouter une variable globale pour stocker le nombre d'issues Osmose
let osmoseIssuesCount = 0;
// Ajouter une variable globale pour stocker le nombre de stations affichées
let displayedStationsCount = 0;
function displayStatsFromGeoJson(resultAsGeojson, stats) {
let count = resultAsGeojson.features.length;
let count_station_output = 0;
let count_ref_eu = 0;
let output_more_than_300 = 0;
let output_more_than_200 = 0;
let output_more_than_100 = 0;
let output_more_than_50 = 0;
let count_station_outputoutput_between_1_and_50 = 0;
let count_output_unknown = 0;
let count_estimated_type2combo = 0;
let count_found_type2combo = 0;
let count_found_type2 = 0;
// Compter les filtres désactivés
let disabledFilters = 0;
Object.keys(lcm_config.filterConfigs).forEach(filterId => {
if (!lcm_config.filterConfigs[filterId]) disabledFilters++;
});
$('#count_features_fond').html('⚡' + count + ' stations' + (disabledFilters > 0 ? ` (${disabledFilters} filtre${disabledFilters > 1 ? 's' : ''} désactivé${disabledFilters > 1 ? 's' : ''}, ${stats.count_hidden_by_filters} masqué${stats.count_hidden_by_filters > 1 ? 's' : ''})` : ''));
resultAsGeojson.features.map(feature => {
let found_type2_combo = false;
let found_type2 = false;
let keys_of_object = Object.keys(feature.properties.tags);
keys_of_object.map(tagKey => {
if (tagKey.indexOf('type2_combo') !== -1) {
found_type2_combo = true;
}
if (tagKey.indexOf('type2') !== -1) {
found_type2 = true;
}
});
let outputPower = lcm_utils.guessOutputPowerFromFeature(feature);
if (found_type2_combo) {
count_found_type2combo++;
}
if (found_type2) {
count_found_type2++;
}
if (outputPower == 0) {
count_output_unknown++;
}
// filtres
// filtrer les valeurs inconnues
if (outputPower >= 200 && !found_type2_combo) {
count_estimated_type2combo++;
}
if (outputPower > 0 && outputPower < 50) {
count_station_outputoutput_between_1_and_50++;
}
if (outputPower >= 50 && outputPower < 100) {
output_more_than_50++;
} else if (outputPower >= 100 && outputPower < 200) {
output_more_than_100++;
} else if (outputPower >= 200 && outputPower < 300) {
output_more_than_200++;
} else if (outputPower >= 300) {
feature.properties.puissance_haute = true;
output_more_than_300++;
}
if (feature.properties.tags['charging_station:output']) {
count_station_output++;
}
if (feature.properties.tags['ref:EU:EVSE']) {
count_ref_eu++;
}
});
let bar_powers = `<div class="bars-container">
<div class="bar color-unknown" style="width: ${calculerPourcentage(count_output_unknown, count, true)}%">${count_output_unknown}</div>
<div class="bar color-power-lesser-than-50" style="width: ${calculerPourcentage(count_station_outputoutput_between_1_and_50, count, true)}%">${count_station_outputoutput_between_1_and_50 ? count_station_outputoutput_between_1_and_50 : ''}</div>
<div class="bar color-power-lesser-than-100" style="width: ${calculerPourcentage(output_more_than_50, count, true)}%">${output_more_than_50 ? output_more_than_50 : ''}</div>
<div class="bar color-power-lesser-than-200" style="width: ${calculerPourcentage(output_more_than_100, count, true)}%">${output_more_than_100 ? output_more_than_100 : ''}</div>
<div class="bar color-power-lesser-than-300" style="width: ${calculerPourcentage(output_more_than_200, count, true)}%">${output_more_than_200 ? output_more_than_200 : '' | ''}</div>
<div class="bar color-power-lesser-than-max" style="width: ${calculerPourcentage(output_more_than_300, count, true)}%">${output_more_than_300 ? output_more_than_300 : ''}</div>
</div>`;
let stats_content = `<div class="stats-table">
<table style="width: 100%;">
<tr>
<th>Type</th>
<th>Nombre</th>
<th>Pourcentage</th>
</tr>
<tr>
<td>Puissance inconnue</td>
<td>${count_output_unknown}</td>
<td>${calculerPourcentage(count_output_unknown, count)}%</td>
</tr>
<tr>
<td>1-50 kW</td>
<td>${count_station_outputoutput_between_1_and_50}</td>
<td>${calculerPourcentage(count_station_outputoutput_between_1_and_50, count)}%</td>
</tr>
<tr>
<td>50-100 kW</td>
<td>${output_more_than_50}</td>
<td>${calculerPourcentage(output_more_than_50, count)}%</td>
</tr>
<tr>
<td>100-200 kW</td>
<td>${output_more_than_100}</td>
<td>${calculerPourcentage(output_more_than_100, count)}%</td>
</tr>
<tr>
<td>200-300 kW</td>
<td>${output_more_than_200}</td>
<td>${calculerPourcentage(output_more_than_200, count)}%</td>
</tr>
<tr>
<td>300+ kW</td>
<td>${output_more_than_300}</td>
<td>${calculerPourcentage(output_more_than_300, count)}%</td>
</tr>
</table>
</div>`;
$('#found_charging_stations').html(stats_content);
$('#bars_power').html(bar_powers);
// Remplacer la ligne existante par un appel à updateCounters
updateCounters();
}
// Ajouter une fonction pour mettre à jour les compteurs
function updateCounters() {
const stationsCount = geojsondata ? geojsondata.features.length : 0;
const osmoseText = lcm_config.osmoseIssuesList.length > 0 ? ` <span class="osmose-counter">(+ ${lcm_config.osmoseIssuesList.length} ?)</span>` : '';
$('#count_features_fond').html(`${stationsCount}${osmoseText} stations`);
}
// Modifier bindEventsOnJosmRemote pour cibler les boutons dans un contexte (la popup)
function bindEventsOnJosmRemote(popupElement) {
// Cible tous les liens JOSM à l'intérieur de l'élément popup fourni
$('#current_station_infos').find('.edit-button.josm').each(function () {
// Utiliser .off().on() pour éviter les liaisons multiples si la popup est rouverte
$(this).off('click').on('click', (event) => {
event.preventDefault();
let josm_link = $(this).attr('data-href');
console.log('Sending command to JOSM:', josm_link);
fetch(josm_link)
.then(response => {
if (!response.ok) {
// Gérer les erreurs de communication avec JOSM
console.error('JOSM remote control error:', response.status, response.statusText);
alert('Erreur de communication avec JOSM. Assurez-vous que JOSM est lancé et que le contrôle à distance est activé.');
} else {
console.log('JOSM command sent successfully.');
// Optionnel: Afficher une notification de succès
}
})
.catch(error => {
console.error('Failed to send command to JOSM:', error);
alert('Impossible d\'envoyer la commande à JOSM. Est-il lancé et le contrôle à distance activé ?');
});
});
});
}
function displayPointsFromApi(points, convert_to_osm_json) {
if (points && convert_to_osm_json) {
geojsondata = osmtogeojson(points);
}
// Réinitialiser le compteur avant d'afficher les points
displayedStationsCount = 0;
displayStatsFromGeoJson(geojsondata);
let stats = {
count_hidden_by_filters: 0
};
displayStatsFromGeoJson(geojsondata, stats);
let resultLayer = L.geoJson(geojsondata, {
style: function (feature) {
return { color: '#f00' };
},
filter: function (feature, layer) {
let isPolygon = (feature.geometry) && (feature.geometry.type !== undefined) && (feature.geometry.type === 'Polygon');
if (isPolygon) {
feature.geometry.type = 'Point';
let polygonCenter = L.latLngBounds(feature.geometry.coordinates[0]).getCenter();
feature.geometry.coordinates = [polygonCenter.lat, polygonCenter.lng];
}
return true;
},
onmoveend: function (event) {
// console.log('déplacement terminé');
},
onzoomend: function (event) {
supprimerMarqueurs();
displayPointsFromApi();
},
onEachFeature: eachFeature,
});
updateFilteredStationsCount();
calculerEtAfficherItineraire(endItinerary);
}
function displaySocketsList(feature) {
let popupContent = '';
popupContent += '<div class="sockets-list" >'
let type2 = feature.properties.tags['socket:type2']
let type2_combo = feature.properties.tags['socket:type2_combo']
if (type2) {
popupContent += ' <img class="icon-img" src="img/Type2_socket.svg" alt="prise de type 2">'
if (type2 !== 'yes') {
popupContent += '<span class="socket-counter">x ' + type2 + '</span>'
}
}
if (feature.properties.tags['socket:type2_combo']) {
popupContent += ' <img class="icon-img" src="img/type2_combo.svg" alt="prise de type 2 combo CCS">'
if (type2_combo !== 'yes') {
popupContent += '<span class="socket-counter">x ' + type2_combo + '</span>'
}
}
popupContent += '</div>'
return popupContent;
}
function makePopupOfFeature(feature) {
let popupContent = ''
popupContent += '<div class="key-values" >'
// ne montrer que certains champs dans la popup
lcm_config.tags_to_display_in_popup.forEach(function (key) {
if (lcm_config.tags_to_display_in_popup.indexOf(key)) {
let value = feature.properties.tags[key]
if (value) {
if (value.indexOf('http') !== -1) {
value = '<a href="' + value + '">' + value + '</a>'
}
popupContent = popupContent + '<br/><strong class="popup-key">' + key + ' :</strong><span class="popup-value">' + value + '</span>'
}
}
})
popupContent += '</div>'
// Ajouter l'affichage des tarifs si l'option est activée
if ($('#display_charges').is(':checked') && feature.properties.tags.charge) {
popupContent += '<div class="charge-info"><strong>Tarifs :</strong> ' + feature.properties.tags.charge + '</div>'
}
return popupContent;
}
/**
* application des filtres dans la sélection des bornes à afficher
* @param feature
* @param layer
*/
function eachFeature(feature, layer, stats) {
const maxPowerFilter = parseInt($('#filter_max_output').val()) || lcm_config.filter_max_output_default_value;
let outPowerGuessed = lcm_utils.guessOutputPowerFromFeature(feature);
// Filtrage par puissance
if (outPowerGuessed === 0 || outPowerGuessed === null) {
if (display_unknown_max_power_station === 'hide') {
return;
}
} else {
// Filtrer les stations dont la puissance dépasse le maximum défini
if (outPowerGuessed < maxPowerFilter) {
return;
}
}
// Incrémenter le compteur de stations affichées
displayedStationsCount++;
let popupContent = makePopupOfFeature(feature);
layer.bindPopup(popupContent);
// Vérifier les filtres activés
if (lcm_config.filterCCS && !feature.properties.tags['socket:type2_combo']) {
stats.count_hidden_by_filters++;
return;
}
if (lcm_config.filterType2 && !feature.properties.tags['socket:type2']) {
stats.count_hidden_by_filters++;
return;
}
if (lcm_config.filterDomestic && !feature.properties.tags['socket:typee']) {
stats.count_hidden_by_filters++;
return;
}
if (lcm_config.filterChademo && !feature.properties.tags['socket:chademo']) {
stats.count_hidden_by_filters++;
return;
}
if (lcm_config.filterType1 && !feature.properties.tags['socket:type1']) {
stats.count_hidden_by_filters++;
return;
}
if (lcm_config.filterType3 && !feature.properties.tags['socket:type3']) {
stats.count_hidden_by_filters++;
return;
}
if (lcm_config.filterCableAttached) {
let hasCableAttached = false;
// Vérifier si une des prises a un câble attaché
['socket:type2:cable', 'socket:type2_combo:cable', 'socket:chademo:cable', 'socket:typee:cable', 'socket:type1:cable', 'socket:type3:cable'].forEach(tag => {
if (feature.properties.tags[tag] === 'yes') {
hasCableAttached = true;
}
});
if (!hasCableAttached) {
stats.count_hidden_by_filters++;
return;
}
}
let displayOutPowerGuessed = '? kW';
if (outPowerGuessed) {
displayOutPowerGuessed = outPowerGuessed + ' kW max';
if (display_unknown_max_power_station === 'show_only') {
return;
}
} else {
// on cache cette station si on ne veut pas voir les stations à la puissance inconnue
if (display_unknown_max_power_station === 'hide') {
return;
}
}
/**
* bornes sans informations, suggérer d'ajouter des tags dans OSM
*/
if (!popupContent) {
popupContent = `<span class="no-data"> Aucune information renseignée,
<a class="edit-button" href="https://www.openstreetmap.org/edit?editor=remote&node=${feature.properties.id}">ajoutez la dans OpenStreetMap!</a></span>`;
}
// Calcul du temps de recharge
let rechargeTimeText = '';
if (outPowerGuessed && outPowerGuessed > 0) {
const hours = averageChargeKwh / outPowerGuessed;
const minutes = Math.round(hours * 60);
const h = Math.floor(minutes / 60);
const m = minutes % 60;
rechargeTimeText = `<div class="recharge-time">
⏱️ + ${averageChargeKwh} kWh en <strong>${h > 0 ? h + 'h ' : ''}${m} min</strong>
</div>`;
}
// contenu de la popup
let html = `<span class="color-indication" style="background-color: ${lcm_color_utils.getColor(feature)};">${displayOutPowerGuessed}</span>
${rechargeTimeText}
<div class="popup-content">
<div class="socket-list">
${displaySocketsList(feature)}
</div>
<div class="other-infos">
<!-- ${popupContent}-->
</div>
</div>
`
let zoom = map.getZoom();
let radius = 20;
let opacity = 0.5;
let ratio_circle = 10;
if (zoom < 13) {
ratio_circle = 5;
} else if (zoom < 15) {
ratio_circle = 1;
opacity = 0.25;
} else if (zoom <= 16) {
ratio_circle = 0.5;
} else if (zoom <= 18) {
ratio_circle = 0.25;
}
if (!layer._latlng) {
if (lastLatLng) {
layer._latlng = lastLatLng;
}
} else {
lastLatLng = layer._latlng;
}
if (!outPowerGuessed) {
radius = radius * ratio_circle;
} else {
/**
* limiter la taille du cercle pour les valeurs aberrantes
* les mettre en valeur en les plafonnant à 1 de plus que le maximum attendu en lcm_config
*/
if (outPowerGuessed > lcm_config.max_possible_station_output) {
console.error("valeur suspecte outPowerGuessed", outPowerGuessed, feature)
outPowerGuessed = lcm_config.max_possible_station_output + 1
}
radius = outPowerGuessed * ratio_circle;
}
/**
* gestion des marqueurs d'alertes
*/
// info de câble manquant
if (display_alert_cable_missing) {
let keys = Object.keys(feature.properties)
/**
* on considère l'information de câble manquante uniquement dans le cas où une info de socket de type 2 est présente mais pas le tag socket:type2_cable.
*/
if (keys.indexOf('socket:type2') !== -1 && keys.indexOf('socket:type2_cable') === -1) {
let circle_alert = L.circle(layer._latlng, {
color: 'red',
fillColor: 'orange',
fillOpacity: 1,
colorOpacity: 0.5,
radius: 20
})
circle_alert.bindPopup("information de câble manquante");
circle_alert.on({
mouseover: function () {
this.openPopup();
bindEventsOnJosmRemote(this.getPopup().getElement());
},
mouseout: function () {
// setTimeout(() => this.closePopup(), 15000);
},
click: function () {
this.openPopup();
bindEventsOnJosmRemote(this.getPopup().getElement());
},
});
circle_alert.addTo(all_stations_markers);
}
}
/**
* affichage des marqueurs de stations de recharge
*/
let circle = L.circle(layer._latlng, {
color: lcm_color_utils.getColor(feature),
fillColor: lcm_color_utils.getColor(feature),
fillOpacity: opacity,
colorOpacity: opacity,
radius: radius
}).addTo(all_stations_markers);
if (zoom > 15) {
let circle_center = L.circle(layer._latlng, {
color: 'black',
fillColor: lcm_color_utils.getColor(feature),
fillOpacity: 1,
radius: 0.1
}).addTo(all_stations_markers);
}
let badtags = lcm_utils.displayBadTagsFromFeature(feature);
if (badtags !== valid_qa_message) {
let circle_alert = L.circle(layer._latlng, {
color: 'red',
fillColor: 'orange',
fillOpacity: 0.5,
radius: radius * 0.85
});
circle_alert.bindTooltip(badtags, {
// permanent: true,
direction: 'top'
}).addTo(bad_tags_markers);
}
circle.bindPopup(html, {
autoPan: false,
closeOnClick: false
});
circle.on({
mouseover: function () {
this.openPopup();
fillDetailsWithFeature(feature);
bindEventsOnJosmRemote(this.getPopup().getElement());
},
mouseout: function () {
// setTimeout(() => this.closePopup(), 15000);
},
click: function () {
this.openPopup();
// Remplir automatiquement #current_station_infos lors du clic
fillDetailsWithFeature(feature);
bindEventsOnJosmRemote(this.getPopup().getElement());
},
});
}
/**
* Remplit le contenu de #current_station_infos avec les informations de la station
* @param {*} feature
*/
function fillDetailsWithFeature(feature) {
// Stocker le feature courant pour pouvoir rafraîchir le détail si besoin
$('#current_station_infos').data('currentFeature', feature);
// Ajout du lien vers Panoramax
const panoramaxLink = `https://api.panoramax.xyz/#focus=map&map=16.7/${feature.geometry.coordinates[1]}/${feature.geometry.coordinates[0]}&speed=250`;
let link_josm = createJOSMEditLink(feature);
let outPowerGuessed = lcm_utils.guessOutputPowerFromFeature(feature);
let displayOutPowerGuessed = '? kW';
if (outPowerGuessed) {
displayOutPowerGuessed = outPowerGuessed + ' kW max';
}
// AJOUT : Calcul du temps de recharge pour le panneau latéral
let rechargeTimeText = '';
if (outPowerGuessed && outPowerGuessed > 0) {
const hours = averageChargeKwh / outPowerGuessed;
const minutes = Math.round(hours * 60);
const h = Math.floor(minutes / 60);
const m = minutes % 60;
rechargeTimeText = `<div class="recharge-time">
⏱️ <strong>${h > 0 ? h + 'h ' : ''}${m} min</strong> pour ${averageChargeKwh} kWh à ${outPowerGuessed} kW:
</div>`;
} else {
rechargeTimeText = `<div class="recharge-time">⏱️ Temps estimé: puissance inconnue</div>`;
}
let content = '';
let table_details = '';
let count_features_in_table = 0;
table_details += '<div class="key-values" >'
// ne montrer que certains champs dans la popup
lcm_config.tags_to_display_in_popup.forEach((key) => {
if (lcm_config.tags_to_display_in_popup.indexOf(key)) {
let value = feature.properties.tags[key]
if (value) {
if (value.indexOf('http') !== -1) {
value = '<a href="' + value + '">' + value + '</a>'
}
table_details += '<br/><strong class="popup-key">' + key + ' :</strong><span class="popup-value">' + value + '</span>'
count_features_in_table++;
}
}
})
table_details += '</div>'
if (!count_features_in_table) {
table_details += '<div class="no-feature-in-table">Aucune information renseignée</div>'
}
// panel de détails dans le volet latéral
content += `
<span class="color-indication" style="background-color: ${lcm_color_utils.getColor(feature)};">${displayOutPowerGuessed}</span>
<div class="buttons-land ">
<a href="https://www.openstreetmap.org/directions?from=&to=${feature.geometry.coordinates[1]},${feature.geometry.coordinates[0]}&engine=fossgis_osrm_car#map=14/${feature.geometry.coordinates[1]}/${feature.geometry.coordinates[0]}" class="navigation-link by-car" title="itinéraire en voiture vers cette station"> 🚗</a>
<a href="https://www.openstreetmap.org/directions?from=&to=${feature.geometry.coordinates[1]},${feature.geometry.coordinates[0]}&engine=fossgis_osrm_bike#map=14/${feature.geometry.coordinates[1]}/${feature.geometry.coordinates[0]}" class="navigation-link by-car" title="itinéraire en vélo vers cette station">🚴‍♀️</a>
<a href="https://www.openstreetmap.org/directions?from=&to=${feature.geometry.coordinates[1]},${feature.geometry.coordinates[0]}&engine=fossgis_osrm_foot#map=14/${feature.geometry.coordinates[1]}/${feature.geometry.coordinates[0]}" class="navigation-link by-car" title="itinéraire à pied vers cette station">👠</a>
<a class="edit-button" href="https://www.openstreetmap.org/edit?editor=id&node=${feature.properties.id}">✏️</a>
<a class="edit-button josm" data-href="${link_josm}" href="#">JOSM</a>
<a href="${makeMapCompleteUrl(feature)}" target="_blank" class="edit-button mapcomplete-link" title="Voir sur MapComplete">
<img src="https://mapcomplete.org/assets/themes/charging_stations/plug.svg" alt="icone">
</a>
<a href="${panoramaxLink}" target="_blank" class="panoramax-link" title="Voir sur Panoramax">
<img src="styles/images/panoramax.ico" alt="icone">
</a>
</div>
${rechargeTimeText}
<div class="socket-list">
${displaySocketsList(feature)}
</div>
<div class="table-details" >
${table_details}
</div>
<div class="bad-tags">
<h3>Problèmes de qualité</h3>
${lcm_utils.displayBadTagsFromFeature(feature)}
</div>
`
$('#current_station_infos').html(`<div class='island irve-details'><h2>Détails</h2>${content}</div>`);
}
function makeMapCompleteUrl(feature) {
// https://mapcomplete.org/charging_stations.html?z=16.3&lat=48.85770772656571&lon=2.353630884174322#node/123454656
const center = map.getCenter()
const zoom = map.getZoom()
return `https://mapcomplete.org/charging_stations.html?z=${zoom}&lat=${center.lat}&lon=${center.lng}#node/${feature.properties.id}`
}
function bindFullDetails(feature) {
$('#fullDetails').on('click', () => {
$('#current_station_infos')[0].innerHTML = `<h2>Détails</h2>
${makePopupOfFeature(feature)}
`
})
}
let isLoading = false
function loadOverpassQuery() {
if (!isLoading) {
isLoading = true;
$('#spinning_icon').fadeIn();
let queryTextfieldValue = $('#query-textfield').val();
let overpassApiUrl = buildOverpassApiUrl(map, queryTextfieldValue);
$.get(overpassApiUrl, function (geoDataPointsFromApi) {
geojsondata = geoDataPointsFromApi;
refreshDisplay(true);
$('#spinning_icon').fadeOut();
$('#message-loading').fadeOut();
isLoading = false;
});
}
}
function refreshDisplay(convert_points_to_osm = false) {
supprimerMarqueurs();
count_hidden_by_filters = 0; // Réinitialiser le compteur
if (geojsondata) {
displayPointsFromApi(geojsondata, convert_points_to_osm);
updateCounters();
updateFilteredStationsCount();
}
// Mettre à jour le compteur dans la popup
let count = geojsondata.features.length;
let disabledFilters = 0;
Object.keys(lcm_config.filterConfigs).forEach(filterId => {
if (!lcm_config.filterConfigs[filterId]) disabledFilters++;
});
$('#count_features_fond').html('⚡' + count + ' stations' + (disabledFilters > 0 ? ` (${disabledFilters} filtre${disabledFilters > 1 ? 's' : ''} désactivé${disabledFilters > 1 ? 's' : ''}, ${count_hidden_by_filters} masqué${count_hidden_by_filters > 1 ? 's' : ''})` : ''));
}
function onMapMoveEnd() {
let center = map.getCenter()
let zoom = map.getZoom()
let infos = `Lat: ${center.lat}, Lon: ${center.lng}, Zoom : ${zoom}`
updateURLWithMapCoordinatesAndZoom();
if (zoom < 10) {
$('#zoomMessage').show()
} else {
$('#zoomMessage').hide()
loadOverpassQuery()
}
if (map.getZoom() > 14) {
searchFoodPlaces(map);
} else {
food_places_markers.clearLayers();
}
$('#infos_carte').html(infos)
// Stocker les dernières coordonnées connues
if (!window.lastKnownPosition) {
window.lastKnownPosition = center;
} else {
// Calculer la distance en km entre l'ancienne et la nouvelle position
const distanceKm = map.distance(center, window.lastKnownPosition) / 1000;
// console.log('déplacement de ', distanceKm, 'km')
// Ne mettre à jour que si on s'est déplacé de plus de 2km
if (distanceKm > 2) {
window.lastKnownPosition = center;
updateURLWithMapCoordinatesAndZoom();
}
}
// Ajout d'un log pour déboguer
console.log("Zoom actuel:", map.getZoom());
if (map.getZoom() >= 12) {
// console.log("Recherche des issues Osmose...");
searchOsmoseIssues(map);
} else {
// console.log("Zoom trop faible pour les issues Osmose");
osmose_markers.clearLayers();
}
}
$(document).ready(function () {
// Charger le service de traduction
init()
});
function showActiveFilter(filterVariableName, selectorId) {
$(selectorId).attr('class', 'filter-state-' + filterVariableName)
}
/**
* mettre à jour les liens vers des éditeurs externes
*/
function updateExternalEditorsLinks() {
const center = map.getCenter()
const zoom = map.getZoom()
mapCompleteLink(center.lat, center.lng, zoom)
idEditorLink(center.lat, center.lng, zoom)
}
function mapCompleteLink(lat, lon, zoom) {
$("mapCompleteLink").attr('href', `https://mapcomplete.org/charging_stations?z=${zoom}&lat=${lat}&lon=${lon}`)
}
function idEditorLink(lat, lon, zoom) {
let href = `https://www.openstreetmap.org/edit?editor=id#map=${zoom}/${lat}/${lon}`
$("idEditorLink").attr('href', href)
}
function cycleVariableState(filterVariableName, selectorId) {
console.log('filterVariableName', filterVariableName, filterStatesAvailable)
if (filterVariableName) {
if (filterVariableName == filterStatesAvailable[0]) {
filterVariableName = filterStatesAvailable[1]
} else if (filterVariableName == filterStatesAvailable[1]) {
filterVariableName = filterStatesAvailable[2]
} else if (filterVariableName == filterStatesAvailable[2]) {
filterVariableName = filterStatesAvailable[0]
}
} else {
filterVariableName = filterStatesAvailable[0]
}
showActiveFilter(filterVariableName, selectorId)
console.log('filterVariableName after', filterVariableName)
return filterVariableName
}
// toggle des stats
$('#toggle-stats').on('click', function () {
$('#found_charging_stations').slideToggle();
// Change le symbole de la flèche
let text = $(this).text();
if (text.includes('🔽')) {
$(this).text(text.replace('🔽', '🔼'));
} else {
$(this).text(text.replace('🔼', '🔽'));
}
});
// Ajouter ces variables avec les autres déclarations globales
let food_places_markers = L.layerGroup();
const foodIcon = L.divIcon({
className: 'food-marker',
html: '🍽️',
iconSize: [20, 20],
iconAnchor: [10, 10]
});
// Ajouter cette fonction avec les autres fonctions de recherche
function searchFoodPlaces(map) {
const bounds = map.getBounds();
const bbox = bounds.getSouth() + ',' + bounds.getWest() + ',' + bounds.getNorth() + ',' + bounds.getEast();
const query = `
[out:json][timeout:25];
(
nwr["amenity"="restaurant"](${bbox});
nwr["amenity"="cafe"](${bbox});
);
out center;`;
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`;
food_places_markers.clearLayers();
fetch(url)
.then(response => response.json())
.then(data => {
data.elements.forEach(element => {
// Utiliser les coordonnées du centre pour les ways et relations
const lat = element.lat || element.center.lat;
const lon = element.lon || element.center.lon;
const name = element.tags.name || 'Sans nom';
const type = element.tags.amenity;
const marker = L.marker([lat, lon], {
icon: foodIcon
});
marker.bindPopup(`
<strong>${name}</strong><br>
Type: ${type}<br>
${element.tags.cuisine ? 'Cuisine: ' + element.tags.cuisine : ''}
`);
food_places_markers.addLayer(marker);
});
})
.catch(error => console.error('Erreur lors de la recherche des restaurants:', error));
}
// Ajouter après la déclaration des autres variables globales
let osmose_markers = L.layerGroup();
const osmoseIcon = L.divIcon({
className: 'osmose-marker-drop',
html: '<div class="osmose-marker-inner">⚡</div>',
iconSize: [30, 40],
iconAnchor: [15, 40]
});
// Ajouter cette fonction utilitaire pour calculer la distance entre deux points
function calculateDistance(lat1, lon1, lat2, lon2) {
return L.latLng(lat1, lon1).distanceTo([lat2, lon2]);
}
// Ajouter cette fonction pour vérifier si le calque des stations est actif
function isChargingStationLayerActive() {
return map.hasLayer(all_stations_markers);
}
// Modifier la fonction searchOsmoseIssues
function searchOsmoseIssues(map) {
const bounds = map.getBounds();
const zoom = map.getZoom();
const bbox = `${bounds.getWest()}%2C${bounds.getSouth()}%2C${bounds.getEast()}%2C${bounds.getNorth()}`;
const url = `https://osmose.openstreetmap.fr/api/0.3/issues?zoom=${zoom}&item=8410%2C8411&level=1%2C2%2C3&limit=500&bbox=${bbox}`;
osmose_markers.clearLayers();
// Modifier la vérification des stations existantes
const existingStations = [];
if (lcm_config.hide_osmose_markers_if_close_to_existing_charging_stations &&
isChargingStationLayerActive() &&
geojsondata &&
geojsondata.features) {
geojsondata.features.forEach(feature => {
if (feature.geometry && feature.geometry.coordinates) {
existingStations.push({
lat: feature.geometry.coordinates[1],
lon: feature.geometry.coordinates[0]
});
}
});
}
fetch(url)
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(`Erreur HTTP ${response.status}: ${response.statusText}. Réponse : ${text}`); });
}
return response.json();
})
.then(data => {
if (!data || !Array.isArray(data.issues)) {
console.warn("Réponse Osmose (liste) inattendue ou pas d'issues:", data);
osmoseIssuesCount = 0;
updateCounters();
return;
}
const issuesList = data.issues;
lcm_config.osmoseIssuesList = [];
osmoseIssuesCount = issuesList.length;
issuesList.forEach(issueInfo => {
// Vérifier que les coordonnées existent et sont valides
if (!issueInfo || issueInfo.lat == null || issueInfo.lon == null || !issueInfo.id) {
console.warn("Issue Osmose invalide:", issueInfo);
return;
}
const lat = parseFloat(issueInfo.lat);
const lon = parseFloat(issueInfo.lon);
// Vérifier que les coordonnées sont des nombres valides
if (isNaN(lat) || isNaN(lon)) {
console.warn("Coordonnées invalides pour l'issue Osmose:", issueInfo);
return;
}
// Vérifier la distance avec les stations existantes
if (lcm_config.hide_osmose_markers_if_close_to_existing_charging_stations) {
const tooClose = existingStations.some(station => {
const distance = calculateDistance(lat, lon, station.lat, station.lon);
return distance <= lcm_config.hide_osmose_markers_if_close_to_existing_charging_stations_distance;
});
if (tooClose) {
return;
}
}
lcm_config.osmoseIssuesList.push(issueInfo);
// Créer le marqueur Osmose
const osmoseMarker = L.circle([lat, lon], {
color: "purple",
fillColor: "purple",
fillOpacity: 0.8,
radius: 10,
className: 'leaflet-osmose-layer',
pane: 'markerPane',
issueId: issueInfo.id
});
// Préparer une popup de chargement simple
osmoseMarker.bindPopup("Chargement des détails...");
osmoseMarker.on('click', function (e) {
const clickedMarker = e.target;
const storedIssueId = clickedMarker.options.issueId;
if (!storedIssueId) {
console.warn("ID d'issue manquant pour le marqueur:", clickedMarker);
return;
}
const detailUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${storedIssueId}?langs=auto`;
console.log("Récupération des détails pour l'issue:", detailUrl);
fetch(detailUrl)
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(`Erreur HTTP ${response.status}: ${response.statusText}. Réponse : ${text}`); });
}
return response.json();
})
.then(issueDetails => {
if (!issueDetails) {
throw new Error("Détails de l'issue non reçus");
}
// Construire le contenu de la popup avec les détails
let popupContent = `<strong>${issueDetails.title?.auto || 'Titre non disponible'}</strong><br>`;
if (issueDetails.subtitle?.auto) {
popupContent += `<p>${issueDetails.subtitle.auto}</p>`;
}
let proposedTags = '';
if (issueDetails.new_elems && issueDetails.new_elems[0] && issueDetails.new_elems[0].add) {
proposedTags = '<table class="proposed-tags">';
issueDetails.new_elems[0].add.forEach(tag => {
proposedTags += `<tr><td>${tag.k}</td><td>${tag.v}</td></tr>`;
});
proposedTags += '</table>';
}
const josmFixUrl = `http://localhost:8111/import?url=https://osmose.openstreetmap.fr/api/0.3/issue/${storedIssueId}/fix/0`;
let josm_buttons = `
<div class="action-buttons">
<a class="edit-button josm" data-href="${josmFixUrl}" href="#" title="Ouvre JOSM et charge la correction proposée">⚡ Réparer dans JOSM</a>
<a href="https://osmose.openstreetmap.fr/fr/issue/${storedIssueId}" target="_blank" title="Voir les détails de l'alerte sur le site Osmose">Voir sur Osmose</a>
</div>`;
popupContent += josm_buttons;
clickedMarker.setPopupContent(popupContent);
$('#current_station_infos').html(`<div class='island osmose-details'><h2>Analyse Osmose</h2>${josm_buttons}
${proposedTags}
</div>`);
bindEventsOnJosmRemote(clickedMarker.getPopup().getElement());
})
.catch(error => {
console.error("Erreur lors de la récupération des détails de l'issue Osmose:", error);
clickedMarker.setPopupContent(`Erreur lors du chargement des détails.<br><a href="https://osmose.openstreetmap.fr/fr/issue/${storedIssueId}" target="_blank">Voir sur Osmose</a>`);
});
});
osmose_markers.addLayer(osmoseMarker);
});
updateCounters();
})
.catch(error => {
console.error('Erreur détaillée lors de la recherche de la liste des issues Osmose:', error);
osmoseIssuesCount = 0;
updateCounters();
});
}
// Ajouter un écouteur d'événements pour le changement de visibilité des calques
function init() {
bindEventsOnJosmRemote();
onMapMoveEnd();
map.on('moveend', onMapMoveEnd);
$('#spinning_icon').hide();
// ... dans la gestion du clic sur un résultat Addok ...
$('.route-to-place').on('click', function() {
const place = $(this).data('place');
calculerEtAfficherItineraire(place, 'car');
});
/**
* boutons de changement de filtres et de rechargement des bornes à l'affichage
*/
$('#removeMarkers').on('click', function () {
supprimerMarqueurs();
});
$('#load').on('click', function () {
loadOverpassQuery();
});
$('#toggleSidePanel').on('click', function () {
console.log('toggleSidePanel', $(this))
$('body').toggleClass('side-panel-open');
});
$('#chercherButton').on('click', function () {
supprimerMarqueurs();
loadOverpassQuery();
geoDataPointsFromApi();
});
$('#setRandomView').on('click', function () {
setRandomView();
loadOverpassQuery();
geoDataPointsFromApi();
});
$('#filterUnkown').on('click', function () {
display_unknown_max_power_station = cycleVariableState(display_unknown_max_power_station, '#filterUnkown');
showActiveFilter(display_unknown_max_power_station, '#filterUnkown');
refreshDisplay();
});
/**
* toggle des alertes de tags décrivant la présence de cable
*/
$('#cableMissing').on('click', function () {
display_alert_cable_missing = !display_alert_cable_missing;
showActiveFilter(display_alert_cable_missing, '#cableMissing');
refreshDisplay();
});
showActiveFilter(display_unknown_max_power_station, '#filterUnkown');
$('#shareUrl').on('click', copyCurrentUrl);
// Initialisation des états des checkboxes des filtres selon les valeurs de configuration
Object.keys(lcm_config.filterConfigs).forEach(filterId => {
$(`#${filterId}`).prop('checked', lcm_config.filterConfigs[filterId]);
});
// Écouteurs pour les filtres
Object.keys(lcm_config.filterConfigs).forEach(filterId => {
$(`#${filterId}`).on('change', function () {
lcm_config[lcm_config.filterConfigs[filterId]] = this.checked;
refreshDisplay();
});
});
$('#filterBadTags').on('click', function () {
lcm_config.display_alert_bad_tags = !lcm_config.display_alert_bad_tags;
showActiveFilter(lcm_config.display_alert_bad_tags, '#filterBadTags');
if (lcm_config.display_alert_bad_tags) {
bad_tags_markers.clearLayers();
bad_tags_markers.addTo(map);
} else {
bad_tags_markers.remove();
}
refreshDisplay();
});
if (lcm_config.display_restaurants_and_cafes) {
food_places_markers.addTo(map);
}
// Mettre à jour le contrôle des calques
const overlayMaps = {
// ...baseLayers,
// "🗺️ Fond de carte": baseLayers,
"⚡ Stations de recharge": all_stations_markers,
"☕ Restaurants et cafés": food_places_markers,
"💡 Bornes potentielles (Osmose)": osmose_markers,
"💡 Problèmes de qualité": bad_tags_markers
};
const overlayControl = L.control.layers(baseLayers, overlayMaps, {
// collapsed: false,
className: 'leaflet-control-layers overlay-layers',
id: 'overlay-layers-control'
})
.addTo(map);
$('#sendToJOSM').on('click', () => {
sendToJOSM(map, geojsondata)
.then(() => {
console.log('Données envoyées à JOSM avec succès !');
})
.catch(() => {
alert('Erreur : JOSM doit être ouvert avec l\'option "Contrôle à distance" activée');
});
});
$('#josmLink').on('click', () => {
sendToJOSM(map, geojsondata)
.then(() => {
console.log('Données envoyées à JOSM avec succès !');
})
.catch(() => {
alert('Erreur : JOSM doit être ouvert avec l\'option de télécommande "Contrôle à distance" activée dans ses paramètres (accédez-y avec F12)');
});
});
$('#searchButton').on('click', function (e) {
e.preventDefault();
searchLocation();
});
$('#searchLocation').on('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
searchLocation();
}
});
$('#shareUrl').on('click', copyCurrentUrl);
$('#filter_max_output').on('input', function () {
const value = $(this).val();
console.log('filter_max_output', value, $(this));
$('#filter_max_output_display').text(value + ' kW');
refreshDisplay();
});
$('#filter_max_output_slider').on('input', function () {
const value = $(this).val();
lcm_config.filter_max_output_default_value = value;
$('#filter_max_output_display').text(value + ' kW');
refreshDisplay();
});
$('#searchResults').on('change', function () {
const selectedIndex = $(this).eq(0).val();
if (selectedIndex !== null) {
const selectedPlace = $(this).find('option:selected').data('place');
moveToLocation(selectedPlace);
}
});
osmose_markers.addTo(map);
$('#average_charge_kwh').val(averageChargeKwh);
$('#average_charge_kwh').on('input', function () {
averageChargeKwh = parseFloat($(this).val()) || 26;
// Si un détail de station est affiché, le mettre à jour
if ($('#current_station_infos').data('currentFeature')) {
fillDetailsWithFeature($('#current_station_infos').data('currentFeature'));
}
});
}
function copyCurrentUrl() {
const url = window.location.href;
var dummy = document.createElement('input'),
text = window.location.href;
document.body.appendChild(dummy);
dummy.value = text;
dummy.select();
document.execCommand('copy');
document.body.removeChild(dummy);
}
init()
// Créer un nouveau pane pour les marqueurs Osmose avec un zIndex plus élevé
map.createPane('osmosePane');
map.getPane('osmosePane').style.zIndex = 1000;
// Ajouter une nouvelle fonction pour mettre à jour le compteur de stations filtrées
function updateFilteredStationsCount() {
const totalStations = geojsondata ? geojsondata.features.length : 0;
const filterStats = `<div class="filter-stats">${displayedStationsCount} stations sur ${totalStations} trouvées</div>`;
// Mettre à jour ou créer l'élément après le slider
let existingStats = $('.filter-stats');
if (existingStats.length) {
existingStats.replaceWith(filterStats);
} else {
$('#filter_max_output_display').after(filterStats);
}
}
// au clic droit, proposer les points d'itinéraire
map.on('contextmenu', function(e) {
// Créer un menu contextuel simple
let popup = L.popup()
.setLatLng(e.latlng)
.setContent(`
<button onclick="window.setStartMarkerFromPopup([${e.latlng.lat},${e.latlng.lng}])">Choisir comme départ</button><br>
<button onclick="window.setEndMarkerFromPopup([${e.latlng.lat},${e.latlng.lng}])">Choisir comme arrivée</button>
`)
.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;
const maxChargePower = parseFloat($('#max_charge_power').val()) || 50;
// 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 = `<div class="battery-info">
<h3>Niveau de batterie : ${level}%</h3>`;
if (level <= minBatteryLevel) {
popupContent += `<p class="warning">⚠️ Recharge recommandée !</p>`;
}
popupContent += `<p>Distance parcourue : ${(accumulatedDistance + segmentDistance * ratio).toFixed(1)} km</p>
<p>Consommation : ${((accumulatedDistance + segmentDistance * ratio) * consumptionPerKm / 1000).toFixed(1)} kWh</p>
</div>`;
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;
// Calculer les temps de recharge nécessaires
let totalRechargeTimeMinutes = 0;
if (numberOfPauses > 0) {
const pauseInterval = totalDistance / (numberOfPauses + 1);
for (let i = 1; i <= numberOfPauses; i++) {
const pauseDistance = pauseInterval * i;
const nextPauseDistance = (i < numberOfPauses) ? pauseInterval : (totalDistance - pauseDistance);
// Calculer la consommation jusqu'à la prochaine pause
const consumptionToNextPause = (nextPauseDistance / 1000) * consumptionPerKm / 1000; // en kWh
const batteryLevelNeeded = (consumptionToNextPause / batteryCapacity) * 100 + minBatteryLevel;
// Calculer le niveau de batterie actuel
const currentBatteryLevel = ((batteryStartKwh - (pauseDistance / 1000 * consumptionPerKm / 1000)) / batteryCapacity) * 100;
// Calculer la quantité d'énergie à recharger
const energyToRecharge = (batteryLevelNeeded - currentBatteryLevel) * batteryCapacity / 100;
// Calculer le temps de recharge nécessaire
const rechargeTimeHours = energyToRecharge / maxChargePower;
const rechargeTimeMinutes = Math.ceil(rechargeTimeHours * 60);
// Ajouter le temps de recharge au total (minimum 10 minutes)
totalRechargeTimeMinutes += Math.max(rechargeTimeMinutes, pauseDurationMinutes);
}
}
// Convertir les différentes durées en format lisible
const formatDuration = (minutes) => {
if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}h${mins > 0 ? ` ${mins}min` : ''}`;
}
return `${minutes} min`;
};
const durationNoPause = formatDuration(durationMin);
const durationWithPauses = formatDuration(durationMin + totalPauseTimeMinutes);
const durationWithRecharge = formatDuration(durationMin + totalRechargeTimeMinutes);
$('#routing_infos').html(`
<section class="routing">
<h2>Itinéraire</h2>
<div>Distance: <strong>${distanceKm} km</strong></div>
<div class="battery-info">
<h3>Niveaux de batterie :</h3>
<div>Départ : <strong>${batteryStartLevel}%</strong></div>
<div>Arrivée : <strong>${batteryEndLevel.toFixed(1)}%</strong></div>
</div>
<div class="duration-info">
<h3>Durées estimées :</h3>
<div>Sans pause : <strong>${durationNoPause}</strong></div>
<div>Avec pauses (10 min) : <strong>${durationWithPauses}</strong></div>
<div>Avec recharges : <strong>${durationWithRecharge}</strong></div>
</div>
<div>Nombre de pauses recommandées: <strong>${numberOfPauses}</strong> (10 min toutes les 2h)</div>
<div>Consommation totale estimée: <strong>${totalConsumptionKwh.toFixed(1)} kWh</strong></div>
<div>
<button onclick="calculerEtAfficherItineraire(window.lastRouteDestination, 'car')">🚗 Voiture</button>
<button onclick="calculerEtAfficherItineraire(window.lastRouteDestination, 'bike')">🚴 Vélo</button>
<button onclick="calculerEtAfficherItineraire(window.lastRouteDestination, 'foot')">🚶 Piéton</button>
</div>
</section>
`);
// 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: '🟢<br>départ',
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: '🔴<br>arrivée',
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();
});
}