From 0b760c20bcabf92c02ca010329ffecf9414dcb1a Mon Sep 17 00:00:00 2001 From: Tykayn Date: Tue, 15 Jul 2025 23:23:32 +0200 Subject: [PATCH] =?UTF-8?q?up=20infos=20v=C3=A9lo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AdminController.php | 70 ++++++++++- .../_followup_bicycle_parking_extra.html.twig | 117 +++++++++++------- .../admin/_followup_cameras_extra.html.twig | 52 ++++++++ templates/admin/_labourage_time_ago.html.twig | 16 +++ .../admin/followup_theme_graph.html.twig | 13 +- templates/admin/stats.html.twig | 5 + templates/public/dashboard.html.twig | 23 ++++ 7 files changed, 246 insertions(+), 50 deletions(-) create mode 100644 templates/admin/_followup_cameras_extra.html.twig create mode 100644 templates/admin/_labourage_time_ago.html.twig diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 53e8fda0..36acab70 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -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]); diff --git a/templates/admin/_followup_bicycle_parking_extra.html.twig b/templates/admin/_followup_bicycle_parking_extra.html.twig index 19826e16..5d59a63a 100644 --- a/templates/admin/_followup_bicycle_parking_extra.html.twig +++ b/templates/admin/_followup_bicycle_parking_extra.html.twig @@ -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 += `
+
Mobilité et stationnement : synthèse
+ + + `; + if (population > 0) { + const ratio = (capacitySum / population).toFixed(4); + html += ``; + } + if (withCapacity > 0) { + html += ``; + } else { + html += ``; + } + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + // Bloc coûts vélo + html += ``; + // Coût estimé parkings voiture + let coutParkingVoiture = car_parking_capacity * 5000; + html += ``; + html += ``; + html += ``; + html += ``; + // Coût d'entretien annuel routes auto + let coutEntretienKm = 13000; // 13 000 €/km/an + let coutEntretienTotal = len_routes * coutEntretienKm; + html += ``; + html += `
Parkings vélo${total}
Parkings vélo par habitant${ratio}
Capacité totale vélo${capacitySum}
Aucune capacité renseignée sur les parkings vélo
Surface estimée parkings vélo (m²)${bike_parking_surface.toLocaleString()}
Voies cyclables séparées (km)${len_separees.toFixed(2)}
Voies cyclables sur route (km)${len_melangees.toFixed(2)}
Parkings voiture${car_parkings}
Capacité totale voiture${car_parking_capacity}
Surface estimée parkings voiture (m²)${car_parking_surface.toLocaleString()}
Longueur totale de routes (km)${len_routes.toFixed(2)}
Coût estimé parkings vélo${coutParking.toLocaleString()} €
Coût estimé parkings voiture${coutParkingVoiture.toLocaleString()} €
Coût pistes cyclables sur chaussée${coutLane.toLocaleString()} €
Coût pistes cyclables séparées${coutTrack.toLocaleString()} €
Total cumulé estimé vélo${totalCoutVelo.toLocaleString()} €
Coût d'entretien annuel routes auto${coutEntretienTotal.toLocaleString()} €
`; + 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 += `
-
Mobilité et stationnement : synthèse
- - - `; - if (population > 0) { - const ratio = (total / population).toFixed(4); - html += ``; - } - if (withCapacity > 0) { - html += ``; - } else { - html += ``; - } - html += ``; - html += ``; - html += ``; - html += ``; - html += ``; - html += ``; - html += ``; - html += `
Parkings vélo${total}
Parkings vélo par habitant${ratio}
Capacité totale vélo${capacitySum}
Aucune capacité renseignée sur les parkings vélo
Surface estimée parkings vélo (m²)${bike_parking_surface.toLocaleString()}
Voies cyclables séparées (km)${len_separees.toFixed(2)}
Voies cyclables sur route (km)${len_melangees.toFixed(2)}
Parkings voiture${car_parkings}
Capacité totale voiture${car_parking_capacity}
Surface estimée parkings voiture (m²)${car_parking_surface.toLocaleString()}
Longueur totale de routes (km)${len_routes.toFixed(2)}
`; - document.getElementById('bicycle-parking-extra-info').innerHTML = html; }) .catch(() => { document.getElementById('bicycle-parking-extra-info').innerHTML = 'Erreur lors du chargement des données vélo.'; }); } }); - \ No newline at end of file + + \ No newline at end of file diff --git a/templates/admin/_followup_cameras_extra.html.twig b/templates/admin/_followup_cameras_extra.html.twig new file mode 100644 index 00000000..b8b742cc --- /dev/null +++ b/templates/admin/_followup_cameras_extra.html.twig @@ -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 +#} +
+
+ Infos caméras : parc, ratios et coûts +
+
+
+ Chargement des statistiques caméras... +
+
+ Les coûts sont des ordres de grandeur indicatifs pour la vidéoprotection urbaine (hors maintenance lourde, hors infrastructure réseau). +
+
+
+ \ No newline at end of file diff --git a/templates/admin/_labourage_time_ago.html.twig b/templates/admin/_labourage_time_ago.html.twig new file mode 100644 index 00000000..410e8355 --- /dev/null +++ b/templates/admin/_labourage_time_ago.html.twig @@ -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') %} + + {{ 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 -%} + \ No newline at end of file diff --git a/templates/admin/followup_theme_graph.html.twig b/templates/admin/followup_theme_graph.html.twig index ba81f145..68b9e439 100644 --- a/templates/admin/followup_theme_graph.html.twig +++ b/templates/admin/followup_theme_graph.html.twig @@ -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 %} @@ -481,7 +485,12 @@ +