diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 1a2915e..3b8fbdd 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -206,8 +206,6 @@ final class AdminController extends AbstractController ->setWebsiteCount($stats->getAvecSite()) ->setSiretCount($placesWithSiret) ->setEmailsCount($placesWithEmail) - // ->setAccessibiliteCount($stats->getAvecAccessibilite()) - // ->setNoteCount($stats->getAvecNote()) ->setCompletionPercent($stats->getCompletionPercent()) ->setStats($stats); @@ -249,6 +247,10 @@ final class AdminController extends AbstractController // Récupérer les stats existantes pour la zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); + if (!$stats) { + // Si aucune statistique n'existe pour cette zone, rediriger vers le labourage de la zone + return $this->redirectToRoute('app_admin_labourer', ['insee_code' => $insee_code]); + } $commerces = $stats->getPlaces(); if(!$stats) { @@ -434,11 +436,24 @@ final class AdminController extends AbstractController // Pas de message d'erreur pour le budget, c'est optionnel } - // Récupérer tous les lieux existants de la ville en une seule requête - $existingPlaces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); - $placesByOsmId = []; - foreach ($existingPlaces as $pl) { - $placesByOsmId[$pl->getOsmId()] = $pl; + // OPTIMISATION : Récupérer seulement les osmId et osmKind des lieux existants pour économiser la mémoire + $existingPlacesQuery = $this->entityManager->getRepository(Place::class) + ->createQueryBuilder('p') + ->select('p.osmId, p.osm_kind, p.id') + ->where('p.zip_code = :zip_code') + ->setParameter('zip_code', $insee_code) + ->getQuery(); + + $existingPlacesResult = $existingPlacesQuery->getResult(); + $placesByOsmKey = []; + + foreach ($existingPlacesResult as $placeData) { + + // var_dump($placeData); + // die( ); + // Clé unique combinant osmId ET osmKind pour éviter les conflits entre node/way + $osmKey = $placeData['osm_kind'] . '_' . $placeData['osmId']; + $placesByOsmKey[$osmKey] = $placeData['id']; } // Récupérer toutes les données @@ -449,12 +464,15 @@ final class AdminController extends AbstractController $overpass_osm_ids = array_map(fn($place) => $place['id'], $places_overpass); - $batchSize = 200; + // RÉDUCTION de la taille du batch pour éviter l'explosion mémoire + $batchSize = 10000; $i = 0; + $notFoundOsmKeys = []; foreach ($places_overpass as $placeData) { - // Vérifier si le lieu existe déjà (optimisé) - $existingPlace = $placesByOsmId[$placeData['id']] ?? null; - if (!$existingPlace) { + // Vérifier si le lieu existe déjà (optimisé) - utilise osmKind + osmId + $osmKey = $placeData['type'] . '_' . $placeData['id']; + $existingPlaceId = $placesByOsmKey[$osmKey] ?? null; + if (!$existingPlaceId) { $place = new Place(); $place->setOsmId($placeData['id']) ->setOsmKind($placeData['type']) @@ -483,26 +501,120 @@ final class AdminController extends AbstractController // Générer le contenu de l'email avec le template $emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]); $place->setEmailContent($emailContent); + + // Log des objets non trouvés + $notFoundOsmKeys[] = $osmKey; } elseif ($updateExisting) { - $existingPlace->setDead(false); - $existingPlace->update_place_from_overpass_data($placeData); - $stats->addPlace($existingPlace); - $this->entityManager->persist($existingPlace); - $updatedCount++; + // Charger l'entité existante seulement si nécessaire + $existingPlace = $this->entityManager->getRepository(Place::class)->find($existingPlaceId); + if ($existingPlace) { + $existingPlace->setDead(false); + $existingPlace->update_place_from_overpass_data($placeData); + $stats->addPlace($existingPlace); + $this->entityManager->persist($existingPlace); + $updatedCount++; + } } $i++; - // Flush/clear Doctrine tous les X lieux pour éviter l'explosion mémoire + + // FLUSH/CLEAR plus fréquent pour éviter l'explosion mémoire if (($i % $batchSize) === 0) { $this->entityManager->flush(); $this->entityManager->clear(); + + // Forcer le garbage collector + gc_collect_cycles(); + // Recharger les stats après clear $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); } } + + $deleteMissing = 1; + // SUPPRESSION des lieux non corrélés par leur osm kind et osm id avec les données overpass + if ($deleteMissing) { + // Créer un ensemble des clés osm présentes dans les données overpass + $overpassOsmKeys = []; + foreach ($places_overpass as $placeData) { + $osmKey = $placeData['type'] . '_' . $placeData['id']; + $overpassOsmKeys[$osmKey] = true; + } + + // ÉLIMINER LES DOUBLONS dans les lieux existants avant suppression + $uniquePlacesByOsmKey = []; + $duplicatePlaceIds = []; + + foreach ($placesByOsmKey as $osmKey => $placeId) { + if (isset($uniquePlacesByOsmKey[$osmKey])) { + // Doublon détecté, garder le plus ancien (ID le plus petit) + if ($placeId < $uniquePlacesByOsmKey[$osmKey]) { + $duplicatePlaceIds[] = $uniquePlacesByOsmKey[$osmKey]; + $uniquePlacesByOsmKey[$osmKey] = $placeId; + } else { + $duplicatePlaceIds[] = $placeId; + } + } else { + $uniquePlacesByOsmKey[$osmKey] = $placeId; + } + } + + // Supprimer les doublons détectés + if (!empty($duplicatePlaceIds)) { + $duplicateDeleteQuery = $this->entityManager->createQuery( + 'DELETE FROM App\Entity\Place p WHERE p.id IN (:placeIds)' + ); + $duplicateDeleteQuery->setParameter('placeIds', $duplicatePlaceIds); + $duplicateDeletedCount = $duplicateDeleteQuery->execute(); + } + + // Trouver les lieux existants uniques qui ne sont plus dans overpass + $placesToDelete = []; + foreach ($uniquePlacesByOsmKey as $osmKey => $placeId) { + if (!isset($overpassOsmKeys[$osmKey])) { + $placesToDelete[] = $placeId; + } + } + + // Supprimer les lieux non trouvés dans overpass en une seule requête + if (!empty($placesToDelete)) { + $deleteQuery = $this->entityManager->createQuery( + 'DELETE FROM App\Entity\Place p WHERE p.id IN (:placeIds)' + ); + $deleteQuery->setParameter('placeIds', $placesToDelete); + $deletedCount = $deleteQuery->execute(); + } + } + // Flush final $this->entityManager->flush(); $this->entityManager->clear(); + // NETTOYAGE D'UNICITÉ des Places après le clear pour éliminer les doublons persistants + // Approche en deux étapes pour éviter l'erreur MySQL "target table for update in FROM clause" + + // Étape 1 : Identifier les doublons + $duplicateIdsQuery = $this->entityManager->createQuery( + 'SELECT p.id FROM App\Entity\Place p + WHERE p.id NOT IN ( + SELECT MIN(p2.id) + FROM App\Entity\Place p2 + GROUP BY p2.osmId, p2.osm_kind, p2.zip_code + )' + ); + $duplicateIds = $duplicateIdsQuery->getResult(); + + // Étape 2 : Supprimer les doublons identifiés + if (!empty($duplicateIds)) { + $duplicateIds = array_column($duplicateIds, 'id'); + $duplicateCleanupQuery = $this->entityManager->createQuery( + 'DELETE App\Entity\Place p WHERE p.id IN (:duplicateIds)' + ); + $duplicateCleanupQuery->setParameter('duplicateIds', $duplicateIds); + $duplicateCleanupCount = $duplicateCleanupQuery->execute(); + } else { + $duplicateCleanupCount = 0; + } + // Récupérer tous les commerces de la zone qui n'ont pas été supprimés $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); @@ -615,6 +727,19 @@ final class AdminController extends AbstractController } $message .= ' Zone : '.$stats->getName().' ('.$stats->getZone().').'; $this->addFlash('success', $message); + + // Afficher le log des objets non trouvés à la fin + if (!empty($notFoundOsmKeys)) { + return $this->render('admin/labourage_results.html.twig', [ + 'stats' => $stats, + 'zone' => $insee_code, + 'new_places_counter' => $processedCount, + 'commerces' => $commerces, + 'not_found_osm_keys' => $notFoundOsmKeys + ]); + } + // Sinon, rediriger comme avant + return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]); } catch (\Exception $e) { $this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage()); die(var_dump($e)); @@ -987,4 +1112,47 @@ final class AdminController extends AbstractController $this->addFlash('success', $budgetsMisAJour.' budgets mis à jour.'); return $this->redirectToRoute('app_admin'); } + +#[Route('/admin/podium-contributeurs-osm', name: 'app_admin_podium_contributeurs_osm')] +public function podiumContributeursOsm(): Response +{ + // On suppose que le champ "osmUser" existe sur l'entité Place + $placeRepo = $this->entityManager->getRepository(\App\Entity\Place::class); + + // Récupérer les 10 contributeurs OSM les plus actifs (par nombre de lieux) + $qb = $placeRepo->createQueryBuilder('p') + ->select('p.osm_user, COUNT(p.id) as nb') + ->where('p.osm_user IS NOT NULL') + ->andWhere("p.osm_user != ''") + ->groupBy('p.osm_user') + ->orderBy('nb', 'DESC') + ->setMaxResults(300); + + $podium = $qb->getQuery()->getResult(); + + // Pour chaque utilisateur, calculer le score de complétion moyen + foreach ($podium as &$row) { + $osmUser = $row['osm_user']; + // Récupérer toutes les places de cet utilisateur + $places = $placeRepo->createQueryBuilder('p') + ->where('p.osm_user = :osm_user') + ->setParameter('osm_user', $osmUser) + ->getQuery()->getResult(); + $total = 0; + $sum = 0; + foreach ($places as $place) { + $score = $place->getCompletionPercentage(); + if ($score !== null) { + $sum += $score; + $total++; + } + } + $row['completion_moyen'] = $total > 0 ? round($sum / $total, 1) : null; + } + unset($row); + + return $this->render('admin/podium_contributeurs_osm.html.twig', [ + 'podium' => $podium + ]); +} } diff --git a/templates/admin/podium_contributeurs_osm.html.twig b/templates/admin/podium_contributeurs_osm.html.twig new file mode 100644 index 0000000..f84d8fb --- /dev/null +++ b/templates/admin/podium_contributeurs_osm.html.twig @@ -0,0 +1,46 @@ +{% extends 'base.html.twig' %} + +{% block title %}Podium des contributeurs OSM{% endblock %} + +{% block body %} +
Voici les 10 contributeurs OpenStreetMap ayant ajouté ou modifié le plus de lieux dans la base :
+Le score de complétion moyen correspond à la moyenne du taux de complétion des lieux ajoutés ou modifiés par chaque contributeur.
+# | +Utilisateur OSM | +Nombre de lieux | +Score de complétion moyen | +
---|---|---|---|
{{ loop.index }} | ++ + {{ row.osm_user }} + + | +{{ row.nb }} | ++ {% if row.completion_moyen is not null %} + {{ row.completion_moyen }} % + {% else %} + N/A + {% endif %} + | +
Aucun contributeur trouvé. |