ajout podium, fix stats manquantes

This commit is contained in:
Tykayn 2025-06-26 18:20:43 +02:00 committed by tykayn
parent d2d2ebe0f0
commit c3a9bc52b2
3 changed files with 237 additions and 17 deletions

View file

@ -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
]);
}
}

View file

@ -0,0 +1,46 @@
{% extends 'base.html.twig' %}
{% block title %}Podium des contributeurs OSM{% endblock %}
{% block body %}
<div class="container mt-4">
<h1>Podium des contributeurs OSM</h1>
<p>Voici les 10 contributeurs OpenStreetMap ayant ajouté ou modifié le plus de lieux dans la base :</p>
<p>Le <strong>score de complétion moyen</strong> correspond à la moyenne du taux de complétion des lieux ajoutés ou modifiés par chaque contributeur.</p>
<table class="table table-striped table-bordered mt-4" style="max-width:800px">
<thead class="table-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Utilisateur OSM</th>
<th scope="col">Nombre de lieux</th>
<th scope="col">Score de complétion moyen</th>
</tr>
</thead>
<tbody>
{% for row in podium %}
<tr>
<th scope="row">{{ loop.index }}</th>
<td>
<a href="https://www.openstreetmap.org/user/{{ row.osm_user|e('url') }}" target="_blank">
{{ row.osm_user }}
</a>
</td>
<td>{{ row.nb }}</td>
<td>
{% if row.completion_moyen is not null %}
{{ row.completion_moyen }}&nbsp;%
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="4">Aucun contributeur trouvé.</td></tr>
{% endfor %}
</tbody>
</table>
<a href="{{ path('app_admin') }}" class="btn btn-secondary mt-3"><i class="bi bi-arrow-left"></i> Retour à l'administration</a>
</div>
{% endblock %}

View file

@ -41,6 +41,12 @@
Fraîcheur de la donnée
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ path('app_admin_podium_contributeurs_osm') }}">
<i class="bi bi-trophy-fill"></i>
Podium des contributeurs OSM
</a>
</li>
</ul>
</div>
</div>