up infos vélo

This commit is contained in:
Tykayn 2025-07-15 23:23:32 +02:00 committed by tykayn
parent c0a1780fce
commit 0b760c20bc
7 changed files with 246 additions and 50 deletions

View file

@ -254,6 +254,32 @@ final class AdminController extends AbstractController
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true, $theme);
}
$this->entityManager->flush();
// Après le flush, vérifier s'il n'y a aucun commerce compté
if ($stats->getPlacesCount() < 2) {
// Récupérer les lieux via Motocultrice
try {
$placesData = $this->motocultrice->labourer($insee_code);
$places = $stats->getPlaces();
foreach ($places as $place) {
// Chercher les données correspondantes par OSM ID
foreach ($placesData as $placeData) {
if ($place->getOsmId() == $placeData['id']) {
// Mettre à jour les tags et coordonnées
$place->update_place_from_overpass_data($placeData);
$place->setStat($stats);
$stats->addPlace($place);
$this->entityManager->persist($place);
break;
}
}
}
$this->entityManager->flush();
} catch (\Exception $e) {
// Ignorer les erreurs silencieusement
}
}
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
}
@ -748,6 +774,48 @@ final class AdminController extends AbstractController
// $this->addFlash('error', '3 Aucune stats trouvée pour ce code INSEE.');
// return $this->redirectToRoute('app_public_index');
}
// Compléter le nom si manquant
if (!$stats->getName()) {
$cityName = $this->motocultrice->get_city_osm_from_zip_code($insee_code);
if ($cityName) {
$stats->setName($cityName);
}
}
// Compléter la population si manquante
if (!$stats->getPopulation()) {
try {
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code;
$response = @file_get_contents($apiUrl);
if ($response !== false) {
$data = json_decode($response, true);
if (isset($data['population'])) {
$stats->setPopulation((int)$data['population']);
}
}
} catch (\Exception $e) {}
}
// Compléter le budget si manquant
if (!$stats->getBudgetAnnuel()) {
$budget = $this->budgetService->getBudgetAnnuel($insee_code);
if ($budget !== null) {
$stats->setBudgetAnnuel((string)$budget);
}
}
// Compléter les lieux d'intérêt si manquants (lat/lon)
if (!$stats->getLat() || !$stats->getLon()) {
// On tente de récupérer le centre de la ville via l'API geo.gouv.fr
try {
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code . '?fields=centre';
$response = @file_get_contents($apiUrl);
if ($response !== false) {
$data = json_decode($response, true);
if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) {
$stats->setLon((string)$data['centre']['coordinates'][0]);
$stats->setLat((string)$data['centre']['coordinates'][1]);
}
}
} catch (\Exception $e) {}
}
// Mettre à jour la date de requête de labourage
$stats->setDateLabourageRequested(new \DateTime());
$this->entityManager->persist($stats);
@ -775,7 +843,7 @@ final class AdminController extends AbstractController
// Toujours générer les CityFollowUp (mais ne jamais les supprimer)
// $themes = \App\Service\FollowUpService::getFollowUpThemes();
// foreach (array_keys($themes) as $theme) {
// $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true, $theme);
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true);
// }
$this->entityManager->flush();
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);

View file

@ -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&nbsp;: 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&nbsp;: 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>

View 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&nbsp;: 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&nbsp;: 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>

View 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>

View file

@ -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,

View file

@ -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">

View file

@ -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="Langle 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">