diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php
index 53e8fda..36acab7 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 19826e1..5d59a63 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
+
+
+ Parkings vélo | ${total} |
`;
+ if (population > 0) {
+ const ratio = (capacitySum / population).toFixed(4);
+ html += `Parkings vélo par habitant | ${ratio} |
`;
+ }
+ if (withCapacity > 0) {
+ html += ` Capacité totale vélo | ${capacitySum} |
`;
+ } else {
+ html += `Aucune capacité renseignée sur les parkings vélo |
`;
+ }
+ html += ` Surface estimée parkings vélo (m²) | ${bike_parking_surface.toLocaleString()} |
`;
+ html += ` Voies cyclables séparées (km) | ${len_separees.toFixed(2)} |
`;
+ html += ` Voies cyclables sur route (km) | ${len_melangees.toFixed(2)} |
`;
+ html += ` Parkings voiture | ${car_parkings} |
`;
+ html += ` Capacité totale voiture | ${car_parking_capacity} |
`;
+ html += ` Surface estimée parkings voiture (m²) | ${car_parking_surface.toLocaleString()} |
`;
+ html += ` Longueur totale de routes (km) | ${len_routes.toFixed(2)} |
`;
+ // Bloc coûts vélo
+ html += ` Coût estimé parkings vélo | ${coutParking.toLocaleString()} € ❓ |
`;
+ // Coût estimé parkings voiture
+ let coutParkingVoiture = car_parking_capacity * 5000;
+ html += ` Coût estimé parkings voiture | ${coutParkingVoiture.toLocaleString()} € ❓ |
`;
+ html += ` Coût pistes cyclables sur chaussée | ${coutLane.toLocaleString()} € ❓ |
`;
+ html += ` Coût pistes cyclables séparées | ${coutTrack.toLocaleString()} € ❓ |
`;
+ html += `Total cumulé estimé vélo | ${totalCoutVelo.toLocaleString()} € |
`;
+ // Coût d'entretien annuel routes auto
+ let coutEntretienKm = 13000; // 13 000 €/km/an
+ let coutEntretienTotal = len_routes * coutEntretienKm;
+ html += ` Coût d'entretien annuel routes auto | ${coutEntretienTotal.toLocaleString()} € ❓ |
`;
+ html += `
`;
+ 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
-
-
- Parkings vélo | ${total} |
`;
- if (population > 0) {
- const ratio = (total / population).toFixed(4);
- html += `Parkings vélo par habitant | ${ratio} |
`;
- }
- if (withCapacity > 0) {
- html += ` Capacité totale vélo | ${capacitySum} |
`;
- } else {
- html += `Aucune capacité renseignée sur les parkings vélo |
`;
- }
- html += ` Surface estimée parkings vélo (m²) | ${bike_parking_surface.toLocaleString()} |
`;
- html += ` Voies cyclables séparées (km) | ${len_separees.toFixed(2)} |
`;
- html += ` Voies cyclables sur route (km) | ${len_melangees.toFixed(2)} |
`;
- html += ` Parkings voiture | ${car_parkings} |
`;
- html += ` Capacité totale voiture | ${car_parking_capacity} |
`;
- html += ` Surface estimée parkings voiture (m²) | ${car_parking_surface.toLocaleString()} |
`;
- html += ` Longueur totale de routes (km) | ${len_routes.toFixed(2)} |
`;
- html += `
`;
- 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 0000000..b8b742c
--- /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
+#}
+
+
+
+
+
+ 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 0000000..410e835
--- /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 ba81f14..68b9e43 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 @@
+