diff --git a/migrations/Version20250705104136.php b/migrations/Version20250705104136.php
new file mode 100644
index 00000000..00e7fd3e
--- /dev/null
+++ b/migrations/Version20250705104136.php
@@ -0,0 +1,41 @@
+addSql(<<<'SQL'
+ ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE stats ADD lat NUMERIC(10, 7) DEFAULT NULL, ADD lon NUMERIC(10, 7) DEFAULT NULL
+ SQL);
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql(<<<'SQL'
+ ALTER TABLE stats DROP lat, DROP lon
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
+ SQL);
+ }
+}
diff --git a/src/Command/UpdateCityCoordinatesCommand.php b/src/Command/UpdateCityCoordinatesCommand.php
new file mode 100644
index 00000000..ef259a5d
--- /dev/null
+++ b/src/Command/UpdateCityCoordinatesCommand.php
@@ -0,0 +1,210 @@
+entityManager->getRepository(Stats::class)->findAll();
+ $total = count($stats);
+ $updated = 0;
+ $errors = 0;
+
+ $io->progressStart($total);
+
+ foreach ($stats as $stat) {
+ if ($stat->getZone() && $stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone())) {
+ $cityName = $stat->getName() ?: $stat->getZone();
+
+ // Vérifier si les coordonnées sont déjà présentes
+ if (!$stat->getLat() || !$stat->getLon()) {
+ $coordinates = $this->getCityCoordinates($cityName, $stat->getZone());
+
+ if ($coordinates) {
+ $stat->setLat((string) $coordinates['lat']);
+ $stat->setLon((string) $coordinates['lon']);
+ $this->entityManager->persist($stat);
+ $updated++;
+ $io->text("✓ $cityName ({$stat->getZone()}): " . $coordinates['lat'] . ", " . $coordinates['lon']);
+ } else {
+ $errors++;
+ $io->warning("✗ Impossible de récupérer les coordonnées pour $cityName ({$stat->getZone()})");
+ }
+ }
+ }
+
+ $io->progressAdvance();
+ }
+
+ $this->entityManager->flush();
+ $io->progressFinish();
+
+ $io->success("Mise à jour terminée : $updated villes mises à jour, $errors erreurs");
+
+ return Command::SUCCESS;
+ }
+
+ private function getCityCoordinates(string $cityName, string $inseeCode): ?array
+ {
+ // Stratégie 1: Nominatim
+ $coordinates = $this->getCoordinatesFromNominatim($cityName);
+ if ($coordinates) {
+ return $coordinates;
+ }
+
+ // Stratégie 2: Premier objet Place townhall
+ $coordinates = $this->getCoordinatesFromPlaceTownhall($inseeCode);
+ if ($coordinates) {
+ return $coordinates;
+ }
+
+ // Stratégie 3: N'importe quel objet Place
+ $coordinates = $this->getCoordinatesFromAnyPlace($inseeCode);
+ if ($coordinates) {
+ return $coordinates;
+ }
+
+ // Stratégie 4: Addok
+ return $this->getCoordinatesFromAddok($cityName, $inseeCode);
+ }
+
+ private function getCoordinatesFromNominatim(string $cityName): ?array
+ {
+ $query = urlencode($cityName . ', France');
+ $url = "https://nominatim.openstreetmap.org/search?q={$query}&format=json&limit=1&countrycodes=fr";
+
+ try {
+ usleep(100000); // 0.1 seconde entre les appels
+
+ $context = stream_context_create([
+ 'http' => [
+ 'timeout' => 5,
+ 'user_agent' => 'OSM-Commerces/1.0'
+ ]
+ ]);
+
+ $response = file_get_contents($url, false, $context);
+
+ if ($response === false) {
+ return null;
+ }
+
+ $data = json_decode($response, true);
+
+ if (!empty($data) && isset($data[0]['lat']) && isset($data[0]['lon'])) {
+ return [
+ 'lat' => (float) $data[0]['lat'],
+ 'lon' => (float) $data[0]['lon']
+ ];
+ }
+ } catch (\Exception $e) {
+ // En cas d'erreur, on retourne null
+ }
+
+ return null;
+ }
+
+ private function getCoordinatesFromPlaceTownhall(string $inseeCode): ?array
+ {
+ $places = $this->entityManager->getRepository(\App\Entity\Place::class)
+ ->createQueryBuilder('p')
+ ->where('p.zip_code = :inseeCode')
+ ->andWhere('p.lat IS NOT NULL')
+ ->andWhere('p.lon IS NOT NULL')
+ ->andWhere('p.main_tag = :mainTag')
+ ->setParameter('inseeCode', $inseeCode)
+ ->setParameter('mainTag', 'townhall')
+ ->setMaxResults(1)
+ ->getQuery()
+ ->getResult();
+
+ if (!empty($places)) {
+ $place = $places[0];
+ return [
+ 'lat' => (float) $place->getLat(),
+ 'lon' => (float) $place->getLon()
+ ];
+ }
+
+ return null;
+ }
+
+ private function getCoordinatesFromAnyPlace(string $inseeCode): ?array
+ {
+ $places = $this->entityManager->getRepository(\App\Entity\Place::class)
+ ->createQueryBuilder('p')
+ ->where('p.zip_code = :inseeCode')
+ ->andWhere('p.lat IS NOT NULL')
+ ->andWhere('p.lon IS NOT NULL')
+ ->setParameter('inseeCode', $inseeCode)
+ ->setMaxResults(1)
+ ->getQuery()
+ ->getResult();
+
+ if (!empty($places)) {
+ $place = $places[0];
+ return [
+ 'lat' => (float) $place->getLat(),
+ 'lon' => (float) $place->getLon()
+ ];
+ }
+
+ return null;
+ }
+
+ private function getCoordinatesFromAddok(string $cityName, string $inseeCode): ?array
+ {
+ $query = urlencode($cityName . ', France');
+ $url = "https://demo.addok.xyz/search?q={$query}&limit=1";
+
+ try {
+ $context = stream_context_create([
+ 'http' => [
+ 'timeout' => 5,
+ 'user_agent' => 'OSM-Commerces/1.0'
+ ]
+ ]);
+
+ $response = file_get_contents($url, false, $context);
+
+ if ($response === false) {
+ return null;
+ }
+
+ $data = json_decode($response, true);
+
+ if (!empty($data['features']) && isset($data['features'][0]['geometry']['coordinates'])) {
+ $coordinates = $data['features'][0]['geometry']['coordinates'];
+ return [
+ 'lat' => (float) $coordinates[1], // Addok retourne [lon, lat]
+ 'lon' => (float) $coordinates[0]
+ ];
+ }
+ } catch (\Exception $e) {
+ // En cas d'erreur, on retourne null
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/PublicController.php b/src/Controller/PublicController.php
index fcbf9ab6..92ee2865 100644
--- a/src/Controller/PublicController.php
+++ b/src/Controller/PublicController.php
@@ -122,17 +122,20 @@ class PublicController extends AbstractController
// Préparer les données pour la carte
$citiesForMap = [];
+
foreach ($stats as $stat) {
- if ($stat->getZone() && $stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone())) {
- // Récupérer les coordonnées de la ville via l'API Nominatim
+ if ($stat->getZone() && $stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone()) && $stat->getZone() !== '00000') {
$cityName = $stat->getName() ?: $stat->getZone();
- $coordinates = $this->getCityCoordinates($cityName, $stat->getZone());
- if ($coordinates) {
+ // Utiliser les coordonnées stockées si disponibles
+ if ($stat->getLat() && $stat->getLon()) {
$citiesForMap[] = [
'name' => $cityName,
'zone' => $stat->getZone(),
- 'coordinates' => $coordinates,
+ 'coordinates' => [
+ 'lat' => (float) $stat->getLat(),
+ 'lon' => (float) $stat->getLon()
+ ],
'placesCount' => $stat->getPlacesCount(),
'completionPercent' => $stat->getCompletionPercent(),
'population' => $stat->getPopulation(),
@@ -165,17 +168,36 @@ class PublicController extends AbstractController
$url = "https://nominatim.openstreetmap.org/search?q={$query}&format=json&limit=1&countrycodes=fr";
try {
- $response = file_get_contents($url);
+ // Ajouter un délai pour respecter les limites de l'API Nominatim
+ usleep(100000); // 0.1 seconde entre les appels
+
+ $context = stream_context_create([
+ 'http' => [
+ 'timeout' => 5, // Timeout de 5 secondes
+ 'user_agent' => 'OSM-Commerces/1.0'
+ ]
+ ]);
+
+ $response = file_get_contents($url, false, $context);
+
+ if ($response === false) {
+ error_log("DEBUG: Échec de récupération des coordonnées pour $cityName ($inseeCode)");
+ return null;
+ }
+
$data = json_decode($response, true);
if (!empty($data) && isset($data[0]['lat']) && isset($data[0]['lon'])) {
+ error_log("DEBUG: Coordonnées trouvées pour $cityName ($inseeCode): " . $data[0]['lat'] . ", " . $data[0]['lon']);
return [
'lat' => (float) $data[0]['lat'],
'lon' => (float) $data[0]['lon']
];
+ } else {
+ error_log("DEBUG: Aucune coordonnée trouvée pour $cityName ($inseeCode)");
}
} catch (\Exception $e) {
- // En cas d'erreur, on retourne null
+ error_log("DEBUG: Exception lors de la récupération des coordonnées pour $cityName ($inseeCode): " . $e->getMessage());
}
return null;
@@ -601,4 +623,61 @@ class PublicController extends AbstractController
'logs' => $logs
]);
}
+
+ #[Route('/add-city', name: 'app_public_add_city')]
+ public function addCity(): Response
+ {
+ return $this->render('public/add_city.html.twig', [
+ 'controller_name' => 'PublicController',
+ ]);
+ }
+
+ #[Route('/stats/{insee_code}/followup-graph/{theme}', name: 'app_public_followup_graph', requirements: ['insee_code' => '\\d+', 'theme' => '[a-zA-Z0-9_]+'])]
+ public function publicFollowupGraph(string $insee_code, string $theme): Response
+ {
+ $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
+ if (!$stats) {
+ $this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE.');
+ return $this->redirectToRoute('app_public_index');
+ }
+
+ $themes = \App\Service\FollowUpService::getFollowUpThemes();
+ if (!isset($themes[$theme])) {
+ $this->addFlash('error', 'Thème non reconnu.');
+ return $this->redirectToRoute('app_public_index');
+ }
+
+ // Récupérer toutes les données de followup pour ce thème
+ $followups = $stats->getCityFollowUps();
+ $countData = [];
+ $completionData = [];
+
+ foreach ($followups as $fu) {
+ if ($fu->getName() === $theme . '_count') {
+ $countData[] = [
+ 'date' => $fu->getDate()->format('Y-m-d'),
+ 'value' => $fu->getMeasure()
+ ];
+ }
+ if ($fu->getName() === $theme . '_completion') {
+ $completionData[] = [
+ 'date' => $fu->getDate()->format('Y-m-d'),
+ 'value' => $fu->getMeasure()
+ ];
+ }
+ }
+
+ // Trier par date
+ usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
+ usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
+
+ return $this->render('public/followup_graph.html.twig', [
+ 'stats' => $stats,
+ 'theme' => $theme,
+ 'theme_label' => $themes[$theme],
+ 'count_data' => json_encode($countData),
+ 'completion_data' => json_encode($completionData),
+ 'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
+ ]);
+ }
}
diff --git a/src/Entity/Stats.php b/src/Entity/Stats.php
index 29fb7cee..21f71e15 100644
--- a/src/Entity/Stats.php
+++ b/src/Entity/Stats.php
@@ -98,6 +98,12 @@ class Stats
#[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $budget_annuel = null;
+ #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 7, nullable: true)]
+ private ?string $lat = null;
+
+ #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 7, nullable: true)]
+ private ?string $lon = null;
+
/**
* @var Collection Entrez le code INSEE de votre ville pour l'ajouter à notre base de données {{ stats.name }} ({{ stats.zone }}) Suivi de la progression du nombre d'objets et du taux de complétion Ajouter ma ville
+
+ Retour à l'accueil
+
+ Labourage d'une nouvelle ville
+ {{ theme_label }}
+ Évolution du {{ theme_label|lower }}
+ Nombre d'objets
+
+ Taux de complétion (%)
+
+
Nous vous enverrons un lien unique pour cela par email, et si vous en avez besoin, nous pouvons
vous aider.
Cliquez sur un marqueur pour voir les statistiques de la ville
+Cliquez sur un marqueur pour voir les statistiques de la ville
+