This commit is contained in:
Tykayn 2024-12-28 17:00:21 +01:00 committed by tykayn
parent ce029e2c76
commit bbf1bf3cdd
6 changed files with 336 additions and 71 deletions

View File

@ -1,15 +1,17 @@
# Carte des IRVE filtrable
![libre-charge-map_overview.jpg](libre-charge-map_overview.jpg)
fait avec le données OpenStreetMap (OSM) ainsi que des icones
![libre-charge-map_popup.jpg](libre-charge-map_popup.jpg)
Venez discuter sur le forum OpenStreetMap: https://forum.openstreetmap.org/viewtopic.php?id=69882
développé par tykayn - https://www.cipherbliss.com - à partir d'un squelette d'example pour Leaflet.
Mastodon: https://mastodon.cipherbliss.com/@tykayn
# Fonctionnalités
Affichage des stations de recharge colorées selon leur puissance maximale délivrée sur un totem.
Changement de fond de carte.
# comment ça marche ?
Avec une lib qui affiche un fond de carte sur lequel on peut naviguer et des marqueurs, on demande poliment à un site web, Overpass Turbo, quels sont les points et polygones d'OpenStreetMap correspondant à plusieurs types de restaurants et lieux où l'on peut trouver à manger et à boire à consommer sur place ou à emporter.
@ -39,6 +41,14 @@ let req = 'https://overpass-api.de/api/interpreter?data=[out:json][timeout:25];
'nwr[amenity=charging_station](area.searchArea);' +
'out body geom;'
```
# Travaux en cours
- ouvrir les charging_station zone dans JOSM
- filtres avancés sur le type de prise
- affichage optionnel des restaurants et autres lieux où l'on peut trouver à manger et à boire comme dans MeltingPot. https://www.cipherbliss.com/ou-manger
# sources
Sources disponibles sur https://forge.chapril.org/tykayn/libre-charge-map.git
Carte similaire, celle des cuisines de restaurant: https://forge.chapril.org/tykayn/melting-pot

View File

@ -27,15 +27,20 @@
<body>
<header>
<h1>
<img class="icon-img" src="img/prise-de-courant.png" alt="prise"> Libre Charge Map
</h1>
</header>
<main>
<button id="toggleSidePanel">
</button>
<div id="zoomMessage">
Zoomez pour voir les stations de recharge
</div>
<div id='map'>
<div class='leaflet-control-container'>
<div class='leaflet-top leaflet-right'>
<div class="research_display">
<div class="research_display">
<div id='spinning_icon'>
<svg
id='star'
@ -45,25 +50,29 @@
xmlns='http://www.w3.org/2000/svg'
version='1.1'>
<polygon
fill='red'
stroke='red'
fill='red'
stroke='red'
stroke-width='10'
points='350,75 379,161 469,161 397,215
423,301 350,250 277,301 303,215
231,161 321,161'/>
</svg>
423,301 350,250 277,301 303,215
231,161 321,161'/>
</svg>
</div>
</div>
<div id='map'>
<div class='leaflet-control-container'>
<div class='leaflet-top leaflet-right'>
</div>
</div>
</div>
</main>
<div class="side-panel">
<h1>
<img class="icon-img" src="img/prise-de-courant.png" alt="prise"> Libre Charge Map
</h1>
<div id="bars_power">
</div>
<div id="round_power_legend">
@ -110,6 +119,9 @@
<button id="setRandomView" class="rounded-button">
🎲 Une ville au hasard
</button>
<button id="sendToJOSM" class="rounded-button">
🗺️ Éditer dans JOSM
</button>
<!-- <button id="toggle">-->
<!-- toggle : montrer les stations avec à minima 150kW de puissance dispo-->
@ -117,8 +129,11 @@
<div id="infos_carte"></div>
<div id="filters">
filtres: <br>
<h2>
🔍 Filtres:
</h2>
<div class="filter-group">
qualité
<button id="filterUnkown">kW max inconnu</button>

View File

@ -9,6 +9,35 @@ const config = {
cartodb : 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
stamen : 'https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
transport : 'https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png'
}
}
},
tags_to_display_in_popup: [
'name',
'capacity',
'description',
'date_start',
'charging_station:output',
'socket:type_2',
'socket:type2:output',
'socket:typee',
'socket:typee:output',
'socket:type2_combo',
'socket:type2_combo:output',
'socket:chademo',
'operator', 'ref:EU:EVSE',
'network',
'opening_hours',
'contact',
'phone',
'contact:phone',
'website',
'contact:website',
'ref',
'fee',
'payment',
'payment:contactless',
'authentication:app',
'authentication:debit_card',
]
}
export default config

29
js/editor.js Normal file
View File

@ -0,0 +1,29 @@
/**
* Fonctions liées à l'édition des données OSM et à l'interaction avec JOSM
*/
export function sendToJOSM(map) {
const bounds = map.getBounds();
const bbox = `${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}`;
const josmUrl = `http://127.0.0.1:8111/load_and_zoom?left=${bounds.getWest()}&right=${bounds.getEast()}&top=${bounds.getNorth()}&bottom=${bounds.getSouth()}&select=node[amenity=charging_station]&changeset_hashtags=IRVE&layer_name=irve-depuis-OSM`;
return fetch(josmUrl)
.then(response => {
if (response.ok) {
console.log('Données envoyées à JOSM avec succès');
return true;
} else {
console.error('Erreur : JOSM doit être ouvert avec l\'option "Contrôle à distance" activée');
throw new Error('JOSM non accessible');
}
})
.catch(error => {
console.error('Erreur JOSM:', error);
throw error;
});
}
export default {
sendToJOSM
};

View File

@ -7,6 +7,7 @@
import config from './config.js'
import utils from './utils.js'
import colorUtils from './color-utils.js'
import editor from './editor.js'
console.log('config', config)
let geojsondata;
@ -86,6 +87,10 @@ function updateURLWithMapCoordinatesAndZoom() {
history.replaceState(null, null, url)
}
let all_stations_markers = L.layerGroup().addTo(map) // layer group pour tous les marqueurs
// let stations_much_speed_wow = L.layerGroup().addTo(map) // layer group des stations rapides
var osm = L.tileLayer(config.tileServers.osm, {
attribution: config.osmMention + '&copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
})
@ -113,22 +118,26 @@ var baseLayers = {
// 'OpenCycleMap': cycle,
'Transport': transport
}
let all_stations_markers = L.layerGroup().addTo(map) // layer group pour tous les marqueurs
// let stations_much_speed_wow = L.layerGroup().addTo(map) // layer group des stations rapides
let overlays = {
stations_bof: all_stations_markers
// , stations_much_speed_wow
} // Si vous avez des calques superposables, ajoutez-les ici
}
const layerControl = L.control.layers(baseLayers, overlays, {collapsed: true}).addTo(map)
tileGrey.addTo(map)
function buildOverpassApiUrl(map, overpassQuery) {
let baseUrl = 'https://overpass-api.de/api/interpreter'
let bounds = map.getBounds().getSouth() + ',' + map.getBounds().getWest() + ',' + map.getBounds().getNorth() + ',' + map.getBounds().getEast()
// Ajouter une marge de 2 kilomètres autour des bounds
// Conversion approximative: 1 degré = 111km à l'équateur
const kilometersMarginForLoading = 2
const marginInDegrees = kilometersMarginForLoading / 111 // 2 kilomètres convertis en degrés
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 (config.overrideQuery) {
@ -144,36 +153,8 @@ function buildOverpassApiUrl(map, overpassQuery) {
}
resultUrl = baseUrl + query
return resultUrl
}
}
const tags_to_display_in_popup = [
'name',
'capacity',
'description',
'date_start',
'charging_station:output',
'socket:type_2',
'socket:type2:output',
'socket:typee',
'socket:typee:output',
'socket:type2_combo',
'socket:type2_combo:output',
'socket:chademo',
'operator', 'ref:EU:EVSE',
'network',
'opening_hours',
'contact',
'phone',
'contact:phone',
'website',
'contact:website',
'ref',
'fee',
'payment',
'payment:contactless',
'authentication:app',
'authentication:debit_card',
]
const margin_josm_bbox = 0.00001
function createJOSMEditLink(feature) {
@ -288,6 +269,48 @@ function displayStatsFromGeoJson(resultAsGeojson) {
</div>
`
let stats_content = `<div class="stats-table">
<table>
<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>`
/**
let stats_content = `<div class="stats">
Statistiques des <strong>${count}</strong> stations trouvées: <br/>
${count_station_output} (${calculerPourcentage(count_station_output, count)}%) ont une info de puissance max délivrée <i>charging_station:output</i>. <br/>
@ -300,13 +323,14 @@ ${output_more_than_50} (${calculerPourcentage(output_more_than_50, count)}%) ont
${count_found_type2combo} (${calculerPourcentage(count_found_type2combo, count)}%) ont un prise combo définie <i>*type2_combo*</i>. <br/>
${count_estimated_type2combo} (${calculerPourcentage(count_estimated_type2combo, count)}%) ont une prise combo présumée à partir de la puissance max trouvée mais non spécifiée <i>*type2_combo*</i>. <br/>${count_found_type2} (${calculerPourcentage(count_found_type2, count)}%) ont un prise type2 définie <i>*type2*</i>. <br/>
</div>`
*
*/
$('#found_charging_stations').html(stats_content)
$('#bars_power').html(bar_powers)
}
function bindEventsOnJosmRemote() {
let josm_remote_buttons = $(`.josm`)
let josm_remote_buttons = $(`#sendToJOSM`)
// console.log('josm_remote_buttons', josm_remote_buttons[0])
$(josm_remote_buttons[0]).on('click', () => {
// console.log('link', josm_remote_buttons[0])
@ -383,8 +407,8 @@ function makePopupOfFeature(feature) {
popupContent += '</div>'
popupContent += '<div class="key-values" >'
// ne montrer que certains champs dans la popup
tags_to_display_in_popup.forEach(function (key) {
if (tags_to_display_in_popup.indexOf(key)) {
config.tags_to_display_in_popup.forEach(function (key) {
if (config.tags_to_display_in_popup.indexOf(key)) {
let value = feature.properties.tags[key]
if (value) {
if (value.indexOf('http') !== -1) {
@ -599,7 +623,20 @@ function onMapMoveEnd() {
loadOverpassQuery()
}
$('#infos_carte').html(infos)
updateURLWithMapCoordinatesAndZoom()
// Stocker les dernières coordonnées connues
if (!window.lastKnownPosition) {
window.lastKnownPosition = center;
updateURLWithMapCoordinatesAndZoom();
} else {
// Calculer la distance en km entre l'ancienne et la nouvelle position
const distanceKm = map.distance(center, window.lastKnownPosition) / 1000;
// Ne mettre à jour que si on s'est déplacé de plus de 2km
if (distanceKm > 2) {
window.lastKnownPosition = center;
updateURLWithMapCoordinatesAndZoom();
}
}
}
@ -618,6 +655,9 @@ $(document).ready(function () {
$('#load').on('click', function () {
loadOverpassQuery()
})
$('#toggleSidePanel').on('click', function () {
$('body').toggleClass('side-panel-open')
})
// filtres
// boutons de toggle et de cycle de visibilité
//
@ -643,6 +683,7 @@ function showActiveFilter(filterVariableName, selectorId) {
$(selectorId).attr('class', 'filter-state-' + filterVariableName)
}
function cycleVariableState(filterVariableName, selectorId) {
console.log('filterVariableName', filterVariableName, filterStatesAvailable)
if (filterVariableName) {
@ -674,3 +715,93 @@ $('#toggle-stats').on('click', function() {
$(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];
(
node["amenity"="restaurant"](${bbox});
node["amenity"="cafe"](${bbox});
);
out body;
>;
out skel qt;`;
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`;
food_places_markers.clearLayers();
fetch(url)
.then(response => response.json())
.then(data => {
const geojson = osmtogeojson(data);
geojson.features.forEach(feature => {
const coords = feature.geometry.coordinates;
const properties = feature.properties;
const name = properties.tags.name || 'Sans nom';
const type = properties.tags.amenity;
const marker = L.marker([coords[1], coords[0]], {
icon: foodIcon
});
marker.bindPopup(`
<strong>${name}</strong><br>
Type: ${type}<br>
${properties.tags.cuisine ? 'Cuisine: ' + properties.tags.cuisine : ''}
`);
food_places_markers.addLayer(marker);
});
})
.catch(error => console.error('Erreur lors de la recherche des restaurants:', error));
}
// Modifier la fonction init pour ajouter le contrôle des couches
function init() {
// ... existing map initialization code ...
// Ajouter le groupe de marqueurs à la carte
food_places_markers.addTo(map);
$('#found_charging_stations').hide();
// Ajouter le contrôle des couches
const overlayMaps = {
"Stations de recharge": all_stations_markers,
"Restaurants et cafés": food_places_markers
};
L.control.layers(null, overlayMaps).addTo(map);
// Ajouter l'événement de recherche sur le déplacement de la carte
map.on('moveend', function() {
if (map.getZoom() > 13) { // Ajuster le niveau de zoom selon vos besoins
searchFoodPlaces(map);
} else {
food_places_markers.clearLayers();
}
});
document.getElementById('sendToJOSM').addEventListener('click', () => {
editor.sendToJOSM(map)
.then(() => {
alert('Données envoyées à JOSM avec succès !');
})
.catch(() => {
alert('Erreur : JOSM doit être ouvert avec l\'option "Contrôle à distance" activée');
});
});
}

View File

@ -1,7 +1,7 @@
html, body {
height: 100%;
width: 100%;
background: #ccc;
background: #222;
}
body {
@ -94,7 +94,7 @@ img.leaflet-marker-icon.tag-socket\:type2_yes {
float: right;
}
#chercherButton {
.side-panel button {
min-width: 10em;
}
@ -112,7 +112,7 @@ img.leaflet-marker-icon.tag-socket\:type2_yes {
background: #96b1ea;
}
#chercherButton:hover,
button:hover,
.edit-button:hover {
background: #0d377b;
border: solid 1px #08285c;
@ -149,8 +149,8 @@ a {
#spinning_icon {
position: fixed;
bottom: 11rem;
left: 20.5rem;
top: 0;
left: 0;
z-index: 10;
background: white;
font-size: 2rem;
@ -292,6 +292,7 @@ marqueurs
button {
cursor: pointer;
padding: 0.5rem;
background: white;
}
#bars_power {
@ -302,7 +303,7 @@ button {
.bar {
height: 1em;
text-align: right;
padding: 0.55rem;
padding: 0.35rem;
padding-right: 0.25rem;
float: left;
}
@ -339,6 +340,11 @@ button {
#infos_carte{
padding: 1rem 0;
}
button + button{
margin-left: 1rem;
}
.filter-group button{
padding: 1rem 2rem;
border-radius: 0.25rem;
@ -376,6 +382,9 @@ button {
background-size:contain;
}
#round_power_legend{
font-size: 0.8rem;
}
.side-panel {
font-size: 1rem;
position: fixed;
@ -386,8 +395,36 @@ button {
background: white;
box-shadow: -2px 0 5px rgba(0,0,0,0.2);
overflow-y: auto;
padding: 20px;
padding: 1rem 2rem;
padding-bottom: 15rem;
z-index: 1000;
visibility: hidden;
top: 5.7rem;
width: 26vw;
}
#toggleSidePanel{
position: fixed;
top: 1rem;
right: 2rem;
z-index: 10;
background: white;
padding: 1rem 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
header{
padding-left: 2rem;
color: #666;
}
.side-panel-open .side-panel{
visibility: visible;
}
.side-panel-open #map{
margin-left: 23.5rem;
}
#infos_carte{
clear:both;
}
#zoomMessage{
position: fixed;
@ -404,6 +441,17 @@ button {
animation: rainbow-border 4s linear infinite;
}
header{
background: #222;
position: fixed;
}
header h1{
line-height: 3rem;
}
header img{
float: left;
margin-right: 1rem;
}
@keyframes rainbow-border {
0% { border-left-color: #ff0000; }
17% { border-left-color: #ff8000; }
@ -416,7 +464,10 @@ button {
#map {
z-index: 1;
margin-right: 300px; /* Pour laisser de la place au panneau */
top: 5.55rem;
}
.side-panel #map{
margin-left: 20vw;
}
/* Style pour mobile */