liens de thème dans la page de graphe, thèmes bouche d'égout, micro bibliothèque, parc à jeux

This commit is contained in:
Tykayn 2025-07-14 18:55:53 +02:00 committed by tykayn
parent 0a5814011f
commit 979be016f2
6 changed files with 117 additions and 86 deletions

View file

@ -218,7 +218,7 @@ La commande traite la ville la plus ancienne en attente de labourage, si les res
Pour labourer environ 300 villes par jour, il faut lancer la commande toutes les 5 minutes (24h * 60min / 5 = 288 passages par jour) : Pour labourer environ 300 villes par jour, il faut lancer la commande toutes les 5 minutes (24h * 60min / 5 = 288 passages par jour) :
``` ```
*/5 * * * * cd /chemin/vers/le/projet && php bin/console app:process-labourage-queue >> var/log/labourage_cron.log 2>&1 */5 * * * * cd /poule/encrypted/www/osm-commerces && php bin/console app:process-labourage-queue >> var/log/labourage_cron.log 2>&1
``` ```
Chaque exécution traite une ville si les ressources le permettent. En adaptant la fréquence, vous pouvez ajuster le débit de traitement. Chaque exécution traite une ville si les ressources le permettent. En adaptant la fréquence, vous pouvez ajuster le débit de traitement.

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250714165523 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note LONGTEXT 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);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note LONGTEXT 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);
}
}

View file

@ -243,13 +243,12 @@ final class AdminController extends AbstractController
$this->entityManager->flush(); $this->entityManager->flush();
// Générer les suivis (followups) après la mise à jour des Places // Générer les suivis (followups) après la mise à jour des Places
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager); $themes = \App\Service\FollowUpService::getFollowUpThemes();
foreach (array_keys($themes) as $theme) {
$message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.'; $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true, $theme);
if ($updateExisting) {
$message .= ' ' . $updatedCount . ' lieux existants mis à jour pour la zone ' . $stats->getName() . ' (' . $stats->getZone() . ').';
} }
$this->addFlash('success', $message); $this->entityManager->flush();
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
} }
$this->entityManager->flush(); $this->entityManager->flush();
@ -649,8 +648,9 @@ final class AdminController extends AbstractController
'count_data' => json_encode($countData), 'count_data' => json_encode($countData),
'completion_data' => json_encode($completionData), 'completion_data' => json_encode($completionData),
'icons' => \App\Service\FollowUpService::getFollowUpIcons(), 'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'followup_labels' => $themes,
'geojson' => json_encode($geojson), 'geojson' => json_encode($geojson),
'overpass_query' => $overpass_query | "", 'overpass_query' => $overpass_query,
'josm_url' => $josm_url, 'josm_url' => $josm_url,
'center' => $center, 'center' => $center,
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null, 'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
@ -765,7 +765,10 @@ final class AdminController extends AbstractController
$this->addFlash('warning', "Le serveur est trop sollicité actuellement (RAM insuffisante). La mise à jour des lieux sera effectuée plus tard automatiquement."); $this->addFlash('warning', "Le serveur est trop sollicité actuellement (RAM insuffisante). La mise à jour des lieux sera effectuée plus tard automatiquement.");
} }
// Toujours générer les CityFollowUp (mais ne jamais les supprimer) // Toujours générer les CityFollowUp (mais ne jamais les supprimer)
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true /* disable cleanup: ne supprime rien */); $themes = \App\Service\FollowUpService::getFollowUpThemes();
foreach (array_keys($themes) as $theme) {
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true, $theme);
}
$this->entityManager->flush(); $this->entityManager->flush();
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]); return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
} }

View file

@ -22,7 +22,7 @@ class CityFollowUp
#[ORM\Column] #[ORM\Column]
private ?\DateTime $date = null; private ?\DateTime $date = null;
#[ORM\ManyToOne(targetEntity: Stats::class, inversedBy: 'cityFollowUps')] #[ORM\ManyToOne(targetEntity: Stats::class, inversedBy: 'cityFollowUps', cascade: ['persist'])]
private ?Stats $stats = null; private ?Stats $stats = null;
public function getId(): ?int public function getId(): ?int

View file

@ -68,6 +68,12 @@ class FollowUpService
$objects = []; $objects = [];
} elseif ($type === 'power_pole') { } elseif ($type === 'power_pole') {
$objects = array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'pole') ?? []; $objects = array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'pole') ?? [];
} elseif ($type === 'manhole') {
$objects = array_filter($elements, fn($el) => ($el['tags']['manhole'] ?? null) === 'manhole') ?? [];
} elseif ($type === 'little_free_library') {
$objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'little_free_library') ?? [];
} elseif ($type === 'playground') {
$objects = array_filter($elements, fn($el) => ($el['tags']['leisure'] ?? null) === 'playground') ?? [];
} else { } else {
$objects = []; $objects = [];
} }
@ -148,40 +154,8 @@ class FollowUpService
$em->flush(); $em->flush();
$em->clear(); $em->clear();
// Suppression des mesures redondantes (même valeur consécutive, sauf la dernière) - désactivée si $disableCleanup est true // Suppression des mesures redondantes (même valeur consécutive, sauf la dernière) - désactivée définitivement
if (!$disableCleanup) { // (aucune suppression de CityFollowUp)
$repo = $em->getRepository(CityFollowUp::class);
foreach (array_keys(self::getFollowUpThemes()) as $type) {
foreach (['_count', '_completion'] as $suffix) {
$name = $type . $suffix;
$followups = $repo->createQueryBuilder('f')
->where('f.stats = :stats')
->andWhere('f.name = :name')
->setParameter('stats', $stats)
->setParameter('name', $name)
->orderBy('f.date', 'ASC')
->getQuery()->getResult();
$toDelete = [];
$prev = null;
$n = count($followups);
foreach ($followups as $i => $fu) {
// Si seulement 2 mesures, ne rien supprimer
if ($n == 2) {
break;
}
if ($prev && $fu->getMeasure() === $prev->getMeasure() && $i < $n - 1) {
$toDelete[] = $prev;
}
$prev = $fu;
}
foreach ($toDelete as $del) {
$em->remove($del);
}
}
}
$em->flush();
$em->clear();
}
} }
public function generateGlobalFollowUps(EntityManagerInterface $em): void public function generateGlobalFollowUps(EntityManagerInterface $em): void
@ -296,6 +270,9 @@ class FollowUpService
'tree' => 'Arbres', 'tree' => 'Arbres',
'places' => 'Lieux', 'places' => 'Lieux',
'power_pole' => 'Poteaux électriques', 'power_pole' => 'Poteaux électriques',
'manhole' => "Bouche d'égout",
'little_free_library' => "Micro bibliothèque",
'playground' => "Parc à jeux pour enfants",
]; ];
} }
@ -325,6 +302,9 @@ class FollowUpService
'tree' => 'bi-tree', 'tree' => 'bi-tree',
'places' => 'bi-geo-alt', 'places' => 'bi-geo-alt',
'power_pole' => 'bi-signpost', 'power_pole' => 'bi-signpost',
'manhole' => 'bi-droplet-half',
'little_free_library' => 'bi-book',
'playground' => 'bi-emoji-smile',
]; ];
} }
@ -359,6 +339,9 @@ class FollowUpService
'drinking_water' => 'nwr["amenity"="drinking_water"](area.searchArea);', 'drinking_water' => 'nwr["amenity"="drinking_water"](area.searchArea);',
'tree' => 'nwr["natural"="tree"](area.searchArea);', 'tree' => 'nwr["natural"="tree"](area.searchArea);',
'power_pole' => 'nwr["power"="pole"](area.searchArea);', 'power_pole' => 'nwr["power"="pole"](area.searchArea);',
'manhole' => 'nwr["manhole"](area.searchArea);',
'little_free_library' => 'nwr["amenity"="little_free_library"](area.searchArea);',
'playground' => 'nwr["leisure"="playground"](area.searchArea);',
]; ];
} }
@ -505,6 +488,9 @@ class FollowUpService
'tree' => ['species', 'leaf_type', 'leaf_cycle'], 'tree' => ['species', 'leaf_type', 'leaf_cycle'],
'power_pole' => ['ref', 'material'], 'power_pole' => ['ref', 'material'],
'places' => ['name', 'address', 'opening_hours', 'website', 'phone', 'wheelchair', 'siret'], 'places' => ['name', 'address', 'opening_hours', 'website', 'phone', 'wheelchair', 'siret'],
'manhole' => ['manhole', 'location'],
'little_free_library' => ['amenity', 'operator'],
'playground' => ['playground', 'operator'],
]; ];
} }
} }

View file

@ -187,6 +187,12 @@
</div> </div>
{% if overpass_query is defined %}
<a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-geo"></i> Vérifier sur Overpass Turbo
</a>
{% endif %}
{% if josm_url %} {% if josm_url %}
<a href="{{ josm_url }}" class="btn btn-outline-dark btn-josm" target="_blank"> <a href="{{ josm_url }}" class="btn btn-outline-dark btn-josm" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Ouvrir tous les objets dans JOSM <i class="bi bi-box-arrow-up-right"></i> Ouvrir tous les objets dans JOSM
@ -194,17 +200,13 @@
{% else %} {% else %}
<div class="alert alert-info mb-3">Aucun objet sélectionné pour ce thème, rien à charger dans JOSM.</div> <div class="alert alert-info mb-3">Aucun objet sélectionné pour ce thème, rien à charger dans JOSM.</div>
{% endif %} {% endif %}
{# <label for="basemapSelect" class="form-label mb-1">Fond de carte :</label>
<select id="basemapSelect" class="form-select">
<option value="streets">MapTiler Streets</option>
<option value="satellite">BD Ortho IGN</option>
</select> #}
<div id="themeMap"></div> <div id="themeMap"></div>
{% if geojson is defined %}
{# On n'utilise plus geojson côté PHP, la carte sera alimentée dynamiquement via Overpass en JS #}
{% endif %}
{% if overpass_query is defined %}
<div class="mb-3">
<a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank" class="btn btn-outline-primary mt-2">
<i class="bi bi-geo"></i> Vérifier sur Overpass Turbo
</a>
</div>
{% endif %}
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
@ -224,32 +226,7 @@
<div class="stat-label">Dernière mise à jour</div> <div class="stat-label">Dernière mise à jour</div>
</div> </div>
</div> </div>
<div class="row mb-3">
<div class="col-md-6">
{# <div class="card p-3">
<h5>Progression</h5>
<table class="table table-sm mb-0">
<thead><tr><th>Période</th><th>Nombre</th><th>Complétion (%)</th></tr></thead>
<tbody>
<tr><td>7 jours</td><td>{{ progressions.count['7j'] is not null ? '%+d'|format(progressions.count['7j']) : '' }}</td><td>{{ progressions.completion['7j'] is not null ? '%+.1f'|format(progressions.completion['7j']) : '' }}</td></tr>
<tr><td>30 jours</td><td>{{ progressions.count['30j'] is not null ? '%+d'|format(progressions.count['30j']) : '' }}</td><td>{{ progressions.completion['30j'] is not null ? '%+.1f'|format(progressions.completion['30j']) : '' }}</td></tr>
<tr><td>6 mois</td><td>{{ progressions.count['6m'] is not null ? '%+d'|format(progressions.count['6m']) : '' }}</td><td>{{ progressions.completion['6m'] is not null ? '%+.1f'|format(progressions.completion['6m']) : '' }}</td></tr>
<tr><td>1 an</td><td>{{ progressions.count['1a'] is not null ? '%+d'|format(progressions.count['1a']) : '' }}</td><td>{{ progressions.completion['1a'] is not null ? '%+.1f'|format(progressions.completion['1a']) : '' }}</td></tr>
</tbody>
</table>
</div> #}
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="ms-auto">
<label for="basemapSelect" class="form-label mb-1">Fond de carte :</label>
<select id="basemapSelect" class="form-select">
<option value="streets">MapTiler Streets</option>
<option value="satellite">BD Ortho IGN</option>
</select>
</div>
</div>
</div>
<div class="chart-container"> <div class="chart-container">
<canvas id="themeChart"></canvas> <canvas id="themeChart"></canvas>
@ -290,7 +267,25 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{# Bloc navigation autres thématiques #}
{% if followup_labels is defined and icons is defined %}
<hr>
<div class="mt-4">
<h4>Autres thématiques de suivi :</h4>
<ul class="list-inline">
{% for t, label in followup_labels %}
{% if t != theme %}
<li class="list-inline-item mb-2">
<a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': t}) }}" class="btn btn-outline-secondary">
<i class="bi {{ icons[t]|default('bi-question-circle') }}"></i> {{ label }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
<a href="https://forum.openstreetmap.fr/t/osm-mon-commerce/34403/11" class="btn btn-info suggestion-footer-btn mt-4 mb-2" target="_blank" rel="noopener"> <a href="https://forum.openstreetmap.fr/t/osm-mon-commerce/34403/11" class="btn btn-info suggestion-footer-btn mt-4 mb-2" target="_blank" rel="noopener">
<i class="bi bi-chat-dots"></i> Faire une suggestion <i class="bi bi-chat-dots"></i> Faire une suggestion
@ -429,15 +424,27 @@
// Affichage des tags manquants // Affichage des tags manquants
let missingHtml = ''; let missingHtml = '';
if (missingTags.length > 0) { if (missingTags.length > 0) {
missingHtml = `<div style='color:#b30000;font-size:0.95em;margin-top:4px;'><b>Manque :</b> ${missingTags.map(t => `<code>${t}</code>`).join(', ')}</div>`; missingHtml = `<div style='color:#b30000;font-size:0.95em;margin-top:4px;'><b>Manque :</b> ` + missingTags.map(t => `<a href='https://wiki.openstreetmap.org/wiki/Key:${encodeURIComponent(t)}' target='_blank' rel='noopener'><code>${t}</code></a>`).join(', ') + `</div>`;
} }
// Liens édition JOSM et iD
const josmUrl = `http://127.0.0.1:8111/load_object?objects=${e.type[0].toUpperCase()}${e.id}`;
const idUrl = `https://www.openstreetmap.org/edit?editor=id&${e.type}=${e.id}`;
const popupHtml = `<div style='min-width:180px'> const popupHtml = `<div style='min-width:180px'>
<strong>${e.tags && e.tags.name ? e.tags.name : '(sans nom)'}</strong><br> <h2 class="title is-2">${e.tags && e.tags.name ? e.tags.name : '(sans nom)'}</h2><br>
<span class='text-muted'>${e.type} ${e.id}</span><br>
<span style='font-size:0.95em;'>${e.tags ? Object.entries(e.tags).map(([k,v]) => `<span><b>${k}</b>: ${v}</span>`).join('<br>') : ''}</span><br>
<b>Complétion :</b> ${completion !== null ? completion + '%' : ''} <b>Complétion :</b> ${completion !== null ? completion + '%' : ''}
${missingHtml} ${missingHtml}
<br><a href='https://www.openstreetmap.org/${e.type}/${e.id}' target='_blank'>Voir sur OSM</a> <br>
<a class="btn btn-info" href='https://www.openstreetmap.org/${e.type}/${e.id}' target='_blank'>
<i class="bi bi-planet" ></i>
Voir sur OSM</a>
<br>
<a class="btn btn-info" href='${josmUrl}' target='_blank'> <i class="bi bi-map" ></i> JOSM</a>
<a class="btn btn-info" href='${idUrl}' target='_blank'><i class="bi bi-pencil" ></i>iD</a>
<span style='font-size:0.95em;'>${e.tags ? Object.entries(e.tags).map(([k,v]) => `<span><b>${k}</b>: ${v}</span>`).join('<br>') : ''}</span><br>
</div>`; </div>`;
new maplibregl.Marker({ color: color }) new maplibregl.Marker({ color: color })
.setLngLat([lon, lat]) .setLngLat([lon, lat])