ajout podium, fix stats manquantes
This commit is contained in:
parent
d2d2ebe0f0
commit
c3a9bc52b2
3 changed files with 237 additions and 17 deletions
|
@ -206,8 +206,6 @@ final class AdminController extends AbstractController
|
||||||
->setWebsiteCount($stats->getAvecSite())
|
->setWebsiteCount($stats->getAvecSite())
|
||||||
->setSiretCount($placesWithSiret)
|
->setSiretCount($placesWithSiret)
|
||||||
->setEmailsCount($placesWithEmail)
|
->setEmailsCount($placesWithEmail)
|
||||||
// ->setAccessibiliteCount($stats->getAvecAccessibilite())
|
|
||||||
// ->setNoteCount($stats->getAvecNote())
|
|
||||||
->setCompletionPercent($stats->getCompletionPercent())
|
->setCompletionPercent($stats->getCompletionPercent())
|
||||||
->setStats($stats);
|
->setStats($stats);
|
||||||
|
|
||||||
|
@ -249,6 +247,10 @@ final class AdminController extends AbstractController
|
||||||
|
|
||||||
// Récupérer les stats existantes pour la zone
|
// Récupérer les stats existantes pour la zone
|
||||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
$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();
|
$commerces = $stats->getPlaces();
|
||||||
|
|
||||||
if(!$stats) {
|
if(!$stats) {
|
||||||
|
@ -434,11 +436,24 @@ final class AdminController extends AbstractController
|
||||||
// Pas de message d'erreur pour le budget, c'est optionnel
|
// 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
|
// OPTIMISATION : Récupérer seulement les osmId et osmKind des lieux existants pour économiser la mémoire
|
||||||
$existingPlaces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
$existingPlacesQuery = $this->entityManager->getRepository(Place::class)
|
||||||
$placesByOsmId = [];
|
->createQueryBuilder('p')
|
||||||
foreach ($existingPlaces as $pl) {
|
->select('p.osmId, p.osm_kind, p.id')
|
||||||
$placesByOsmId[$pl->getOsmId()] = $pl;
|
->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
|
// 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);
|
$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;
|
$i = 0;
|
||||||
|
$notFoundOsmKeys = [];
|
||||||
foreach ($places_overpass as $placeData) {
|
foreach ($places_overpass as $placeData) {
|
||||||
// Vérifier si le lieu existe déjà (optimisé)
|
// Vérifier si le lieu existe déjà (optimisé) - utilise osmKind + osmId
|
||||||
$existingPlace = $placesByOsmId[$placeData['id']] ?? null;
|
$osmKey = $placeData['type'] . '_' . $placeData['id'];
|
||||||
if (!$existingPlace) {
|
$existingPlaceId = $placesByOsmKey[$osmKey] ?? null;
|
||||||
|
if (!$existingPlaceId) {
|
||||||
$place = new Place();
|
$place = new Place();
|
||||||
$place->setOsmId($placeData['id'])
|
$place->setOsmId($placeData['id'])
|
||||||
->setOsmKind($placeData['type'])
|
->setOsmKind($placeData['type'])
|
||||||
|
@ -483,26 +501,120 @@ final class AdminController extends AbstractController
|
||||||
// Générer le contenu de l'email avec le template
|
// Générer le contenu de l'email avec le template
|
||||||
$emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]);
|
$emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]);
|
||||||
$place->setEmailContent($emailContent);
|
$place->setEmailContent($emailContent);
|
||||||
|
|
||||||
|
// Log des objets non trouvés
|
||||||
|
$notFoundOsmKeys[] = $osmKey;
|
||||||
} elseif ($updateExisting) {
|
} elseif ($updateExisting) {
|
||||||
$existingPlace->setDead(false);
|
// Charger l'entité existante seulement si nécessaire
|
||||||
$existingPlace->update_place_from_overpass_data($placeData);
|
$existingPlace = $this->entityManager->getRepository(Place::class)->find($existingPlaceId);
|
||||||
$stats->addPlace($existingPlace);
|
if ($existingPlace) {
|
||||||
$this->entityManager->persist($existingPlace);
|
$existingPlace->setDead(false);
|
||||||
$updatedCount++;
|
$existingPlace->update_place_from_overpass_data($placeData);
|
||||||
|
$stats->addPlace($existingPlace);
|
||||||
|
$this->entityManager->persist($existingPlace);
|
||||||
|
$updatedCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$i++;
|
$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) {
|
if (($i % $batchSize) === 0) {
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->entityManager->clear();
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
// Forcer le garbage collector
|
||||||
|
gc_collect_cycles();
|
||||||
|
|
||||||
// Recharger les stats après clear
|
// Recharger les stats après clear
|
||||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
$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
|
// Flush final
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->entityManager->clear();
|
$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
|
// 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]);
|
$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().').';
|
$message .= ' Zone : '.$stats->getName().' ('.$stats->getZone().').';
|
||||||
$this->addFlash('success', $message);
|
$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) {
|
} catch (\Exception $e) {
|
||||||
$this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage());
|
$this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage());
|
||||||
die(var_dump($e));
|
die(var_dump($e));
|
||||||
|
@ -987,4 +1112,47 @@ final class AdminController extends AbstractController
|
||||||
$this->addFlash('success', $budgetsMisAJour.' budgets mis à jour.');
|
$this->addFlash('success', $budgetsMisAJour.' budgets mis à jour.');
|
||||||
return $this->redirectToRoute('app_admin');
|
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
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
46
templates/admin/podium_contributeurs_osm.html.twig
Normal file
46
templates/admin/podium_contributeurs_osm.html.twig
Normal 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 }} %
|
||||||
|
{% 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 %}
|
|
@ -41,6 +41,12 @@
|
||||||
Fraîcheur de la donnée
|
Fraîcheur de la donnée
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue