up speed limit map

This commit is contained in:
Tykayn 2025-09-03 16:38:05 +02:00 committed by tykayn
parent 8c1380ec04
commit 32b803115e
2 changed files with 567 additions and 385 deletions

View file

@ -3,134 +3,316 @@
{% block title %}Limites de vitesse - {{ stats.name }}{% endblock %}
{% block body %}
<div class="container mt-4">
<h1><i class="bi bi-speedometer2"></i> Limites de vitesse à {{ stats.name }} ({{ stats.zone }})</h1>
<p>Complétion des limitations de vitesse sur le réseau routier OSM.<br>
<span class="text-muted">Tags attendus :
<div class="container-fluid">
<div class="row">
<!-- Sidebar de navigation -->
<div class="col-12 col-lg-3">
{{ stats.name }} :
{% include 'admin/_city_sidebar.html.twig' with {'stats': stats, 'active_menu': 'speed-limit'} %}
</div>
<!-- Contenu principal -->
<div class="col-lg-9 col--12 main-content">
<div class="mt-4">
<div class="container mt-4">
<h1><i class="bi bi-speedometer2"></i> Limites de vitesse à {{ stats.name }} ({{ stats.zone }})
</h1>
<p>Complétion des limitations de vitesse sur le réseau routier OSM.<br>
<span class="text-muted">Tags attendus :
{% for tag in expected_tags %}<code>{{ tag }}</code>{% if not loop.last %}, {% endif %}{% endfor %}
</span></p>
<div class="row">
<div class="col-md-8">
<div class="card mb-3">
<div class="card-header"><i class="bi bi-map"></i> Carte des routes (coloration selon maxspeed)</div>
<div class="card-body p-2">
<div id="speedlimit-map" style="height: 450px; width: 100%;"></div>
<div id="speedlimit-completion" class="mt-2"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><i class="bi bi-traffic-cone"></i> Panneaux routiers & feux de circulation</div>
<div class="card-body p-2">
<div id="traffic-map" style="height: 350px; width: 100%;"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><i class="bi bi-info-circle"></i> Informations</div>
<div class="card-body">
<ul>
<li><b>Rouge</b> : route sans <code>maxspeed</code></li>
<li><b>Vert</b> : route avec <code>maxspeed</code></li>
<li><b>Bleu</b> : panneau routier (<code>traffic_sign</code>)</li>
<li><b>Orange</b> : feu de circulation (<code>traffic_signals</code>)</li>
</ul>
<p>Cliquer sur un objet pour ouvrir dans OSM, iD ou JOSM.</p>
<div class="row">
<div class="col-md-8">
<div class="card mb-3">
<div class="card-header"><i class="bi bi-map"></i> Carte des routes (coloration
selon maxspeed)
</div>
<div class="card-body p-2">
<div id="speedlimit-map" style="height: 450px; width: 100%;"></div>
<div id="speedlimit-completion" class="mt-2"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><i class="bi bi-traffic-cone"></i> Panneaux routiers & feux
de circulation
</div>
<div class="card-body p-2">
<div class="row">
<div class="col-md-8">
<div id="traffic-map" style="height: 350px; width: 100%;"></div>
</div>
<div class="col-md-4">
<div id="traffic-stats" class="mt-2">
<h5>Objets trouvés</h5>
<div id="traffic-objects-list">Chargement...</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><i class="bi bi-info-circle"></i> Informations</div>
<div class="card-body">
<ul>
<li><b>Rouge</b> : route sans <code>maxspeed</code></li>
<li><b>Vert</b> : route avec <code>maxspeed</code></li>
<li><b>Bleu</b> : panneau routier (<code>traffic_sign</code>)</li>
<li><b>Orange</b> : feu de circulation (<code>traffic_signals</code>)</li>
</ul>
<p>Cliquer sur un objet pour ouvrir dans OSM, iD ou JOSM.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="/js/maplibre/maplibre-gl.js"></script>
<script>
const insee = '{{ insee_code }}';
const overpassHighways = `[out:json][timeout:60];\narea["ref:INSEE"="${insee}"]->.searchArea;\nway["highway"](area.searchArea);\nout body;\n>;\nout skel qt;`;
const overpassTraffic = `[out:json][timeout:60];\narea["ref:INSEE"="${insee}"]->.searchArea;\n(
{{ parent() }}
<script src="/js/maplibre/maplibre-gl.js"></script>
<script>
const insee = '{{ insee_code }}';
const overpassHighways = `[out:json][timeout:60];\narea["ref:INSEE"="${insee}"]->.searchArea;\nway["highway"~"^(primary|secondary|tertiary|primary_link|secondary_link)$"](area.searchArea);\nout body;\n>;\nout skel qt;`;
const overpassTraffic = `[out:json][timeout:60];\narea["ref:INSEE"="${insee}"]->.searchArea;\n(
node["traffic_sign"](area.searchArea);
node["highway"="traffic_signals"](area.searchArea);
);\nout body;`;
function fetchOverpass(query) {
return fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: query,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}).then(r => r.json());
}
function fetchOverpass(query) {
return fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: query,
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}).then(r => r.json());
}
document.addEventListener('DOMContentLoaded', function() {
// --- Carte des highways ---
let map = new maplibregl.Map({
container: 'speedlimit-map',
style: 'https://api.maptiler.com/maps/streets/style.json?key={{ maptiler_token }}',
center: [2, 48],
zoom: 13
});
map.addControl(new maplibregl.NavigationControl());
fetchOverpass(overpassHighways).then(data => {
let nodes = {};
let withMaxspeed = 0, total = 0;
data.elements.forEach(e => { if (e.type === 'node') nodes[e.id] = e; });
data.elements.forEach(e => {
if (e.type === 'way' && e.tags && e.tags.highway) {
total++;
let hasMaxspeed = !!e.tags.maxspeed;
if (hasMaxspeed) withMaxspeed++;
let coords = e.nodes.map(nid => nodes[nid]).filter(n => n).map(n => [n.lon, n.lat]);
if (coords.length < 2) return;
map.addLayer({
id: 'way-' + e.id,
type: 'line',
source: {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords }, properties: {} }
},
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: {
'line-color': hasMaxspeed ? '#28a745' : '#dc3545',
'line-width': 3
document.addEventListener('DOMContentLoaded', function () {
// --- Carte des highways ---
let map = new maplibregl.Map({
container: 'speedlimit-map',
style: 'https://api.maptiler.com/maps/streets/style.json?key={{ maptiler_token }}',
center: [2, 48],
zoom: 13
});
map.addControl(new maplibregl.NavigationControl());
fetchOverpass(overpassHighways).then(data => {
let nodes = {};
let withMaxspeed = 0, total = 0;
let allCoords = [];
data.elements.forEach(e => {
if (e.type === 'node') nodes[e.id] = e;
});
data.elements.forEach(e => {
if (e.type === 'way' && e.tags && e.tags.highway) {
total++;
let hasMaxspeed = !!e.tags.maxspeed;
if (hasMaxspeed) withMaxspeed++;
let coords = e.nodes.map(nid => nodes[nid]).filter(n => n).map(n => [n.lon, n.lat]);
if (coords.length < 2) return;
// Store all coordinates for calculating bounds
allCoords = allCoords.concat(coords);
map.addLayer({
id: 'way-' + e.id,
type: 'line',
source: {
type: 'geojson',
data: {
type: 'Feature',
geometry: {type: 'LineString', coordinates: coords},
properties: {}
}
},
layout: {'line-join': 'round', 'line-cap': 'round'},
paint: {
'line-color': hasMaxspeed ? '#28a745' : '#dc3545',
'line-width': 3
}
});
// Popup OSM/iD/JOSM
let popupHtml = `<b>Highway ${e.tags.highway}</b><br>ID: ${e.id}`;
if (e.tags.maxspeed) popupHtml += `<br><b>maxspeed:</b> ${e.tags.maxspeed}`;
popupHtml += `<br><a href='https://www.openstreetmap.org/way/${e.id}' target='_blank'>OSM</a> | <a href='https://www.openstreetmap.org/edit?editor=id&way=${e.id}' target='_blank'>iD</a> | <a href='http://127.0.0.1:8111/load_object?objects=W${e.id}' target='_blank'>JOSM</a>`;
let marker = new maplibregl.Marker({color: hasMaxspeed ? '#28a745' : '#dc3545'})
.setLngLat(coords[Math.floor(coords.length / 2)])
.setPopup(new maplibregl.Popup({offset: 18}).setHTML(popupHtml));
// marker.addTo(map); // Optionnel : afficher un marker au milieu
}
});
// Popup OSM/iD/JOSM
let popupHtml = `<b>Highway ${e.tags.highway}</b><br>ID: ${e.id}`;
if (e.tags.maxspeed) popupHtml += `<br><b>maxspeed:</b> ${e.tags.maxspeed}`;
popupHtml += `<br><a href='https://www.openstreetmap.org/way/${e.id}' target='_blank'>OSM</a> | <a href='https://www.openstreetmap.org/edit?editor=id&way=${e.id}' target='_blank'>iD</a> | <a href='http://127.0.0.1:8111/load_object?objects=W${e.id}' target='_blank'>JOSM</a>`;
let marker = new maplibregl.Marker({ color: hasMaxspeed ? '#28a745' : '#dc3545' })
.setLngLat(coords[Math.floor(coords.length/2)])
.setPopup(new maplibregl.Popup({ offset: 18 }).setHTML(popupHtml));
// marker.addTo(map); // Optionnel : afficher un marker au milieu
}
let completion = total > 0 ? Math.round(100 * withMaxspeed / total) : 0;
let withoutMaxspeed = total - withMaxspeed;
// Center map on roads if we have coordinates
if (allCoords.length > 0) {
// Calculate bounds
let bounds = allCoords.reduce((bounds, coord) => {
return [
[Math.min(bounds[0][0], coord[0]), Math.min(bounds[0][1], coord[1])],
[Math.max(bounds[1][0], coord[0]), Math.max(bounds[1][1], coord[1])]
];
}, [[Infinity, Infinity], [-Infinity, -Infinity]]);
// Fit map to bounds with padding
map.fitBounds(bounds, {
padding: 50,
maxZoom: 15
});
}
// Create HTML for completion info and JOSM button
let completionHtml = `
<div class="d-flex justify-content-between align-items-center">
<div>
<b>Complétion maxspeed :</b> ${withMaxspeed} / ${total} (${completion}%)
<br><b>Tronçons sans maxspeed :</b> ${withoutMaxspeed}
</div>
<div>
<button id="open-josm-button" class="btn btn-sm btn-primary" disabled>
<i class="bi bi-box-arrow-up-right"></i> Ouvrir dans JOSM
</button>
</div>
</div>
`;
document.getElementById('speedlimit-completion').innerHTML = completionHtml;
// Store ways without maxspeed for JOSM
let waysWithoutMaxspeed = [];
data.elements.forEach(e => {
if (e.type === 'way' && e.tags && e.tags.highway && !e.tags.maxspeed) {
waysWithoutMaxspeed.push(e.id);
}
});
// Enable JOSM button if there are ways without maxspeed
const josmButton = document.getElementById('open-josm-button');
if (waysWithoutMaxspeed.length > 0) {
josmButton.disabled = false;
josmButton.addEventListener('click', function () {
const objects = waysWithoutMaxspeed.map(id => 'w' + id).join(',');
const josmUrl = `http://127.0.0.1:8111/load_object?objects=${objects}`;
window.open(josmUrl, '_blank');
});
}
});
// --- Carte des panneaux et feux ---
let map2 = new maplibregl.Map({
container: 'traffic-map',
style: 'https://api.maptiler.com/maps/streets/style.json?key={{ maptiler_token }}',
center: [2, 48],
zoom: 13
});
map2.addControl(new maplibregl.NavigationControl());
fetchOverpass(overpassTraffic).then(data => {
// Compteurs pour les différents types d'objets
let objectCounts = {
'traffic_signals': 0,
'traffic_sign': 0
};
// Compteurs pour les différentes valeurs de traffic_sign
let trafficSignTypes = {};
data.elements.forEach(e => {
if (e.type === 'node') {
// Déterminer le type d'objet
let isTrafficSignal = e.tags && e.tags.highway === 'traffic_signals';
let objectType = isTrafficSignal ? 'traffic_signals' : 'traffic_sign';
// Incrémenter le compteur
objectCounts[objectType]++;
// Si c'est un panneau, compter son type
if (!isTrafficSignal && e.tags && e.tags.traffic_sign) {
// Un panneau peut avoir plusieurs types séparés par des points-virgules
let signTypes = e.tags.traffic_sign.split(';');
signTypes.forEach(type => {
type = type.trim();
if (type) {
trafficSignTypes[type] = (trafficSignTypes[type] || 0) + 1;
}
});
}
// Couleur et icône selon le type
let color = isTrafficSignal ? 'orange' : 'blue';
let icon = isTrafficSignal ? '🚦' : '🛑';
// Construire le HTML du popup avec toutes les propriétés
let popupHtml = `<b>${icon} ${isTrafficSignal ? 'Feu de circulation' : 'Panneau routier'}</b><br>ID: ${e.id}`;
// Ajouter toutes les propriétés du panneau
if (e.tags) {
Object.entries(e.tags).forEach(([key, value]) => {
popupHtml += `<br><b>${key}:</b> ${value}`;
});
}
// Liens vers OSM, iD et JOSM
popupHtml += `<br><a href='https://www.openstreetmap.org/node/${e.id}' target='_blank'>OSM</a> | <a href='https://www.openstreetmap.org/edit?editor=id&node=${e.id}' target='_blank'>iD</a> | <a href='http://127.0.0.1:8111/load_object?objects=N${e.id}' target='_blank'>JOSM</a>`;
// Ajouter le marqueur à la carte
new maplibregl.Marker({color: color})
.setLngLat([e.lon, e.lat])
.setPopup(new maplibregl.Popup({offset: 18}).setHTML(popupHtml))
.addTo(map2);
}
});
// Générer le HTML pour la liste des objets trouvés
let totalObjects = objectCounts.traffic_signals + objectCounts.traffic_sign;
let objectsListHtml = `
<div class="list-group">
<div class="list-group-item d-flex justify-content-between align-items-center">
<span>🚦 Feux de circulation</span>
<span class="badge bg-primary rounded-pill">${objectCounts.traffic_signals}</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<span>🛑 Panneaux routiers</span>
<span class="badge bg-primary rounded-pill">${objectCounts.traffic_sign}</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center fw-bold">
<span>Total</span>
<span class="badge bg-success rounded-pill">${totalObjects}</span>
</div>
</div>
`;
// Si des types de panneaux ont été trouvés, les ajouter à la liste
if (Object.keys(trafficSignTypes).length > 0) {
objectsListHtml += `
<h6 class="mt-3">Types de panneaux</h6>
<div class="list-group">
`;
// Trier les types de panneaux par nombre décroissant
let sortedTypes = Object.entries(trafficSignTypes)
.sort((a, b) => b[1] - a[1]);
sortedTypes.forEach(([type, count]) => {
objectsListHtml += `
<div class="list-group-item d-flex justify-content-between align-items-center small">
<span>${type}</span>
<span class="badge bg-secondary rounded-pill">${count}</span>
</div>
`;
});
objectsListHtml += `</div>`;
}
// Mettre à jour la liste des objets
document.getElementById('traffic-objects-list').innerHTML = objectsListHtml;
});
});
let completion = total > 0 ? Math.round(100 * withMaxspeed / total) : 0;
document.getElementById('speedlimit-completion').innerHTML = `<b>Complétion maxspeed :</b> ${withMaxspeed} / ${total} (${completion}%)`;
});
// --- Carte des panneaux et feux ---
let map2 = new maplibregl.Map({
container: 'traffic-map',
style: 'https://api.maptiler.com/maps/streets/style.json?key={{ maptiler_token }}',
center: [2, 48],
zoom: 13
});
map2.addControl(new maplibregl.NavigationControl());
fetchOverpass(overpassTraffic).then(data => {
data.elements.forEach(e => {
if (e.type === 'node') {
let color = e.tags && e.tags.highway === 'traffic_signals' ? 'orange' : 'blue';
let icon = e.tags && e.tags.highway === 'traffic_signals' ? '🚦' : '🛑';
let popupHtml = `<b>${icon} ${e.tags.highway === 'traffic_signals' ? 'Feu de circulation' : 'Panneau routier'}</b><br>ID: ${e.id}`;
if (e.tags.traffic_sign) popupHtml += `<br><b>traffic_sign:</b> ${e.tags.traffic_sign}`;
popupHtml += `<br><a href='https://www.openstreetmap.org/node/${e.id}' target='_blank'>OSM</a> | <a href='https://www.openstreetmap.org/edit?editor=id&node=${e.id}' target='_blank'>iD</a> | <a href='http://127.0.0.1:8111/load_object?objects=N${e.id}' target='_blank'>JOSM</a>`;
new maplibregl.Marker({ color: color })
.setLngLat([e.lon, e.lat])
.setPopup(new maplibregl.Popup({ offset: 18 }).setHTML(popupHtml))
.addTo(map2);
}
});
});
});
</script>
</script>
{% endblock %}