mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
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())
|
||||
->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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
</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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue