up infos vélo
This commit is contained in:
parent
c0a1780fce
commit
0b760c20bc
7 changed files with 246 additions and 50 deletions
|
@ -43,6 +43,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
let car_parking_capacity = 0;
|
||||
let car_parking_surface = 0;
|
||||
let bike_parking_surface = 0;
|
||||
let cyclewayLaneKm = 0;
|
||||
let cyclewayTrackKm = 0;
|
||||
if (data.elements) {
|
||||
// Indexer les nœuds pour calculs de longueur
|
||||
data.elements.forEach(e => {
|
||||
|
@ -107,59 +109,80 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Fonction pour calculer la longueur d'une way en km
|
||||
function wayLengthKm(way) {
|
||||
let len = 0;
|
||||
for (let i = 1; i < way.nodes.length; i++) {
|
||||
const n1 = nodes[way.nodes[i-1]];
|
||||
const n2 = nodes[way.nodes[i]];
|
||||
if (n1 && n2) {
|
||||
// Haversine
|
||||
const R = 6371;
|
||||
const dLat = (n2.lat-n1.lat)*Math.PI/180;
|
||||
const dLon = (n2.lon-n1.lon)*Math.PI/180;
|
||||
const a = Math.sin(dLat/2)*Math.sin(dLat/2) + Math.cos(n1.lat*Math.PI/180)*Math.cos(n2.lat*Math.PI/180)*Math.sin(dLon/2)*Math.sin(dLon/2);
|
||||
const c = 2*Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
len += R*c;
|
||||
// Calculs longueurs (Haversine)
|
||||
function wayLengthKm(way) {
|
||||
let len = 0;
|
||||
for (let i = 1; i < way.nodes.length; i++) {
|
||||
const n1 = nodes[way.nodes[i-1]];
|
||||
const n2 = nodes[way.nodes[i]];
|
||||
if (n1 && n2) {
|
||||
// Haversine
|
||||
const R = 6371;
|
||||
const dLat = (n2.lat-n1.lat)*Math.PI/180;
|
||||
const dLon = (n2.lon-n1.lon)*Math.PI/180;
|
||||
const a = Math.sin(dLat/2)*Math.sin(dLat/2) + Math.cos(n1.lat*Math.PI/180)*Math.cos(n2.lat*Math.PI/180)*Math.sin(dLon/2)*Math.sin(dLon/2);
|
||||
const c = 2*Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
len += R*c;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
return len;
|
||||
let len_separees = 0;
|
||||
let len_melangees = 0;
|
||||
let len_routes = 0;
|
||||
cycleways_separees.forEach(w => { let l = wayLengthKm(w); len_separees += l; cyclewayTrackKm += l; });
|
||||
cycleways_melangees.forEach(w => { let l = wayLengthKm(w); len_melangees += l; cyclewayLaneKm += l; });
|
||||
roads.forEach(w => { len_routes += wayLengthKm(w); });
|
||||
|
||||
// Calculs des coûts vélo
|
||||
let coutParking = capacitySum * 500;
|
||||
let coutLane = cyclewayLaneKm * 10000;
|
||||
let coutTrack = cyclewayTrackKm * 400000;
|
||||
let totalCoutVelo = coutParking + coutLane + coutTrack;
|
||||
|
||||
// Affichage synthétique
|
||||
let html = '';
|
||||
html += `<div class='mb-3 p-2 border rounded bg-light'>
|
||||
<h5><i class='bi bi-bicycle'></i> Mobilité et stationnement : synthèse</h5>
|
||||
<table class='table table-sm align-middle mb-0' style='max-width:700px;'>
|
||||
<tbody>
|
||||
<tr><th><i class='bi bi-bicycle'></i> Parkings vélo</th><td class='text-end'><b>${total}</b></td></tr>`;
|
||||
if (population > 0) {
|
||||
const ratio = (capacitySum / population).toFixed(4);
|
||||
html += `<tr><th class='text-muted'>Parkings vélo par habitant</th><td class='text-end'>${ratio}</td></tr>`;
|
||||
}
|
||||
if (withCapacity > 0) {
|
||||
html += `<tr><th><i class='bi bi-people'></i> Capacité totale vélo</th><td class='text-end'>${capacitySum}</td></tr>`;
|
||||
} else {
|
||||
html += `<tr><th colspan='2' class='text-danger'>Aucune capacité renseignée sur les parkings vélo</th></tr>`;
|
||||
}
|
||||
html += `<tr><th><i class='bi bi-aspect-ratio'></i> Surface estimée parkings vélo (m²)</th><td class='text-end'>${bike_parking_surface.toLocaleString()}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-signpost'></i> Voies cyclables séparées (km)</th><td class='text-end'>${len_separees.toFixed(2)}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-signpost'></i> Voies cyclables sur route (km)</th><td class='text-end'>${len_melangees.toFixed(2)}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-car-front'></i> Parkings voiture</th><td class='text-end'>${car_parkings}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-people'></i> Capacité totale voiture</th><td class='text-end'>${car_parking_capacity}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-aspect-ratio'></i> Surface estimée parkings voiture (m²)</th><td class='text-end'>${car_parking_surface.toLocaleString()}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-road'></i> Longueur totale de routes (km)</th><td class='text-end'>${len_routes.toFixed(2)}</td></tr>`;
|
||||
// Bloc coûts vélo
|
||||
html += `<tr><th><i class='bi bi-cash-coin'></i> Coût estimé parkings vélo</th><td class='text-end'>${coutParking.toLocaleString()} € <span title="Hypothèse : 500 € par place de parking vélo. Source : villes-cyclables.org, asphalte-evolution.fr, routes.fandom.com, https://www.idrrim.com/ressources/documents/source/2/11468-IDRRIM_Rapport_ONR_2024.pdf" style="cursor: help;">❓</span></td></tr>`;
|
||||
// Coût estimé parkings voiture
|
||||
let coutParkingVoiture = car_parking_capacity * 5000;
|
||||
html += `<tr><th><i class='bi bi-cash-coin'></i> Coût estimé parkings voiture</th><td class='text-end'>${coutParkingVoiture.toLocaleString()} € <span title="Hypothèse : 5 000 € par place de parking voiture (hors foncier, VRD, abri). Source : IDRRIM 2024, asphalte-evolution.fr, routes.fandom.com, https://www.idrrim.com/ressources/documents/source/2/11468-IDRRIM_Rapport_ONR_2024.pdf" style='cursor: help;'>❓</span></td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-cash-coin'></i> Coût pistes cyclables sur chaussée</th><td class='text-end'>${coutLane.toLocaleString()} € <span title="Hypothèse : 10 000 € par km de piste cyclable sur chaussée. Source : villes-cyclables.org, asphalte-evolution.fr, routes.fandom.com" style="cursor: help;">❓</span></td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-cash-coin'></i> Coût pistes cyclables séparées</th><td class='text-end'>${coutTrack.toLocaleString()} € <span title="Hypothèse : 400 000 € par km de piste cyclable séparée. Source : villes-cyclables.org, asphalte-evolution.fr, routes.fandom.com" style="cursor: help;">❓</span></td></tr>`;
|
||||
html += `<tr><th><b>Total cumulé estimé vélo</b></th><td class='text-end'><b>${totalCoutVelo.toLocaleString()} €</b></td></tr>`;
|
||||
// Coût d'entretien annuel routes auto
|
||||
let coutEntretienKm = 13000; // 13 000 €/km/an
|
||||
let coutEntretienTotal = len_routes * coutEntretienKm;
|
||||
html += `<tr><th><i class='bi bi-tools'></i> Coût d'entretien annuel routes auto</th><td class='text-end'>${coutEntretienTotal.toLocaleString()} € <span title="Hypothèse : 13 milliards d'euros par an pour 1 million de km de routes en France, soit 13 000 €/km/an. Source : calcul national, IDRRIM 2024, asphalte-evolution.fr, https://www.idrrim.com/ressources/documents/source/2/11468-IDRRIM_Rapport_ONR_2024.pdf" style='cursor: help;'>❓</span></td></tr>`;
|
||||
html += `</tbody></table></div>`;
|
||||
document.getElementById('bicycle-parking-extra-info').innerHTML = html;
|
||||
}
|
||||
let len_separees = 0;
|
||||
let len_melangees = 0;
|
||||
let len_routes = 0;
|
||||
cycleways_separees.forEach(w => { len_separees += wayLengthKm(w); });
|
||||
cycleways_melangees.forEach(w => { len_melangees += wayLengthKm(w); });
|
||||
roads.forEach(w => { len_routes += wayLengthKm(w); });
|
||||
let html = '';
|
||||
html += `<div class='mb-3 p-2 border rounded bg-light'>
|
||||
<h5><i class='bi bi-bicycle'></i> Mobilité et stationnement : synthèse</h5>
|
||||
<table class='table table-sm align-middle mb-0' style='max-width:600px;'>
|
||||
<tbody>
|
||||
<tr><th><i class='bi bi-bicycle'></i> Parkings vélo</th><td class='text-end'><b>${total}</b></td></tr>`;
|
||||
if (population > 0) {
|
||||
const ratio = (total / population).toFixed(4);
|
||||
html += `<tr><th class='text-muted'>Parkings vélo par habitant</th><td class='text-end'>${ratio}</td></tr>`;
|
||||
}
|
||||
if (withCapacity > 0) {
|
||||
html += `<tr><th><i class='bi bi-people'></i> Capacité totale vélo</th><td class='text-end'>${capacitySum}</td></tr>`;
|
||||
} else {
|
||||
html += `<tr><th colspan='2' class='text-danger'>Aucune capacité renseignée sur les parkings vélo</th></tr>`;
|
||||
}
|
||||
html += `<tr><th><i class='bi bi-aspect-ratio'></i> Surface estimée parkings vélo (m²)</th><td class='text-end'>${bike_parking_surface.toLocaleString()}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-signpost'></i> Voies cyclables séparées (km)</th><td class='text-end'>${len_separees.toFixed(2)}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-signpost'></i> Voies cyclables sur route (km)</th><td class='text-end'>${len_melangees.toFixed(2)}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-car-front'></i> Parkings voiture</th><td class='text-end'>${car_parkings}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-people'></i> Capacité totale voiture</th><td class='text-end'>${car_parking_capacity}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-aspect-ratio'></i> Surface estimée parkings voiture (m²)</th><td class='text-end'>${car_parking_surface.toLocaleString()}</td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-road'></i> Longueur totale de routes (km)</th><td class='text-end'>${len_routes.toFixed(2)}</td></tr>`;
|
||||
html += `</tbody></table></div>`;
|
||||
document.getElementById('bicycle-parking-extra-info').innerHTML = html;
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('bicycle-parking-extra-info').innerHTML = '<span class="text-danger">Erreur lors du chargement des données vélo.</span>';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
52
templates/admin/_followup_cameras_extra.html.twig
Normal file
52
templates/admin/_followup_cameras_extra.html.twig
Normal file
|
@ -0,0 +1,52 @@
|
|||
{#
|
||||
Template d'infos supplémentaires pour la thématique "cameras"
|
||||
À inclure dans followup_theme_graph.html.twig si theme == 'cameras'
|
||||
Nécessite que la variable JS "objects" soit disponible (données OSM ou importées)
|
||||
Nécessite que la variable stats.population soit transmise au template parent
|
||||
#}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-camera-video"></i> Infos caméras : parc, ratios et coûts
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div id="cameras-extra-info">
|
||||
<span class="text-muted">Chargement des statistiques caméras...</span>
|
||||
</div>
|
||||
<div class="mt-2 small text-muted">
|
||||
<i class="bi bi-info-circle"></i> Les coûts sont des ordres de grandeur indicatifs pour la vidéoprotection urbaine (hors maintenance lourde, hors infrastructure réseau).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof objects !== 'undefined' && '{{ theme }}' === 'cameras') {
|
||||
const population = {{ stats.population|default(0) }};
|
||||
// Filtrer les caméras (OSM : man_made=surveillance ou surveillance:type=*)
|
||||
let cameras = objects.filter(obj => obj.tags && (obj.tags.man_made === 'surveillance' || obj.tags['surveillance:type']));
|
||||
let nbCameras = cameras.length;
|
||||
let ratio = population > 0 ? (nbCameras / population).toFixed(5) : '—';
|
||||
let coutInstall = nbCameras * 50000;
|
||||
let coutAnnuel = nbCameras * 100000;
|
||||
let total1an = coutInstall + coutAnnuel;
|
||||
let nbCamerasStr = nbCameras.toLocaleString('fr-FR', {minimumFractionDigits: 1, maximumFractionDigits: 1});
|
||||
let ratioStr = population > 0 ? (nbCameras / population).toLocaleString('fr-FR', {minimumFractionDigits: 1, maximumFractionDigits: 1}) : '—';
|
||||
let coutInstallStr = coutInstall.toLocaleString('fr-FR', {minimumFractionDigits: 1, maximumFractionDigits: 1});
|
||||
let coutAnnuelStr = coutAnnuel.toLocaleString('fr-FR', {minimumFractionDigits: 1, maximumFractionDigits: 1});
|
||||
let total1anStr = total1an.toLocaleString('fr-FR', {minimumFractionDigits: 1, maximumFractionDigits: 1});
|
||||
let html = '';
|
||||
html += `<div class='mb-3 p-2 border rounded bg-light'>
|
||||
<h5><i class='bi bi-camera-video'></i> Parc de caméras : synthèse</h5>
|
||||
<table class='table table-sm align-middle mb-0' style='max-width:700px;'>
|
||||
<tbody>
|
||||
<tr><th><i class='bi bi-camera-video'></i> Caméras détectées</th><td class='text-end'><b>${nbCamerasStr}</b></td></tr>`;
|
||||
if (population > 0) {
|
||||
html += `<tr><th class='text-muted'>Caméras par habitant</th><td class='text-end'>${ratioStr}</td></tr>`;
|
||||
}
|
||||
html += `<tr><th><i class='bi bi-cash-coin'></i> Coût d'installation</th><td class='text-end'>${coutInstallStr} € <span title="Hypothèse : 50 000 € d'installation par caméra (matériel, pose, raccordement). Source : estimation marché public, presse spécialisée." style='cursor: help;'>❓</span></td></tr>`;
|
||||
html += `<tr><th><i class='bi bi-cash-coin'></i> Coût annuel services/abonnements</th><td class='text-end'>${coutAnnuelStr} € <span title="Hypothèse : 100 000 € par caméra et par an (maintenance, supervision, abonnements, stockage). Source : estimation marché public, presse spécialisée." style='cursor: help;'>❓</span></td></tr>`;
|
||||
html += `<tr><th><b>Total cumulé sur 1 an</b></th><td class='text-end'><b>${total1anStr} €</b></td></tr>`;
|
||||
html += `</tbody></table></div>`;
|
||||
document.getElementById('cameras-extra-info').innerHTML = html;
|
||||
}
|
||||
});
|
||||
</script>
|
16
templates/admin/_labourage_time_ago.html.twig
Normal file
16
templates/admin/_labourage_time_ago.html.twig
Normal file
|
@ -0,0 +1,16 @@
|
|||
{# Template partiel pour afficher une date et le temps écoulé #}
|
||||
{% set now = "now"|date('U') %}
|
||||
{% set then = date|date('U') %}
|
||||
{% set diff = now - then %}
|
||||
{% set hours = (diff / 3600)|round(0, 'floor') %}
|
||||
{% set days = (diff / 86400)|round(0, 'floor') %}
|
||||
<span title="{{ date|date('d/m/Y H:i') }}">
|
||||
{{ date|date('d/m/Y H:i') }}
|
||||
{%- if days > 0 -%}
|
||||
(il y a {{ days }} jour{{ days > 1 ? 's' : '' }})
|
||||
{%- elseif hours > 0 -%}
|
||||
(il y a {{ hours }} heure{{ hours > 1 ? 's' : '' }})
|
||||
{%- else -%}
|
||||
(il y a moins d'une heure)
|
||||
{%- endif -%}
|
||||
</span>
|
|
@ -192,6 +192,10 @@
|
|||
{% include 'admin/_followup_bicycle_parking_extra.html.twig' %}
|
||||
{% endif %}
|
||||
|
||||
{% if theme == 'camera' %}
|
||||
{% include 'admin/_followup_cameras_extra.html.twig' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if overpass_query is defined %}
|
||||
<a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank" class="btn btn-outline-primary">
|
||||
|
@ -481,7 +485,12 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
const countData = {{ count_data|json_encode|raw }};
|
||||
console.log(countData)
|
||||
|
||||
|
||||
const completionData = {{ completion_data|json_encode|raw }};
|
||||
|
||||
// Mettre à jour les statistiques
|
||||
|
@ -549,7 +558,7 @@
|
|||
datasets: [
|
||||
{
|
||||
label: "Nombre d'objets",
|
||||
data: countData.map(d => ({ x: new Date(d.date), y: d.value })),
|
||||
data: countData?.map(d => ({ x: new Date(d.date), y: d.value })),
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
borderWidth: 2,
|
||||
|
@ -559,7 +568,7 @@
|
|||
},
|
||||
{
|
||||
label: 'Pourcentage de complétion',
|
||||
data: completionData.map(d => ({ x: new Date(d.date), y: d.value })),
|
||||
data: completionData?.map(d => ({ x: new Date(d.date), y: d.value })),
|
||||
borderColor: '#198754',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
borderWidth: 2,
|
||||
|
|
|
@ -185,6 +185,11 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if stats.dateLabourageDone %}
|
||||
<div class="alert alert-info">
|
||||
Dernier labourage : {{ include('admin/_labourage_time_ago.html.twig', { date: stats.dateLabourageDone }) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
|
|
@ -112,6 +112,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-geo-alt"></i> Nombre de villes suivies : <b>{{ stats_list|length }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-secondary">
|
||||
<i class="bi bi-graph-up"></i> Angle de la régression linéaire : <b id="dashboard-regression-angle">—</b>
|
||||
<span title="L’angle est calculé à partir de la pente de la droite de régression sur le graphe principal.">❓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Ce code suppose que regressionSlope est défini ailleurs après le calcul de la régression
|
||||
if (typeof regressionSlope !== 'undefined') {
|
||||
let angle = Math.atan(regressionSlope) * 180 / Math.PI;
|
||||
let angleStr = angle.toLocaleString('fr-FR', {minimumFractionDigits: 1, maximumFractionDigits: 1}) + '°';
|
||||
document.getElementById('dashboard-regression-angle').innerText = angleStr;
|
||||
console.log('Angle de la régression linéaire :', angleStr);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue