page de détail, ajout de mesures

This commit is contained in:
Tykayn 2025-08-12 11:23:20 +02:00 committed by tykayn
parent bcafef75f1
commit af1233c246
2 changed files with 523 additions and 313 deletions

View file

@ -733,12 +733,64 @@ final class AdminController extends AbstractController
$center = [$first->getLon(), $first->getLat()]; $center = [$first->getLon(), $first->getLat()];
} }
// Calculate current metrics from objects array (from Overpass data)
$currentCount = count($objects);
// Calculate current completion percentage
$completionTags = \App\Service\FollowUpService::getFollowUpCompletionTags()[$theme] ?? [];
$currentCompletion = 0;
if ($currentCount > 0 && !empty($completionTags)) {
$totalTags = count($completionTags) * $currentCount;
$filledTags = 0;
foreach ($objects as $obj) {
// Get the original Place object to check tags
$place = null;
foreach ($places as $p) {
if ($p->getOsmId() === $obj['id'] && $p->getOsmKind() === $obj['osm_kind']) {
$place = $p;
break;
}
}
if ($place) {
foreach ($completionTags as $tag) {
// Simple check for name tag
if ($tag === 'name' && !empty($place->getName())) {
$filledTags++;
}
// Add more tag checks as needed
}
}
}
$currentCompletion = $totalTags > 0 ? round(($filledTags / $totalTags) * 100) : 0;
}
// Add current data to history if empty
if (empty($countData)) {
$countData[] = [
'date' => (new \DateTime())->format('Y-m-d'),
'value' => $currentCount
];
}
if (empty($completionData)) {
$completionData[] = [
'date' => (new \DateTime())->format('Y-m-d'),
'value' => $currentCompletion
];
}
return $this->render('admin/followup_theme_graph.html.twig', [ return $this->render('admin/followup_theme_graph.html.twig', [
'stats' => $stats, 'stats' => $stats,
'theme' => $theme, 'theme' => $theme,
'theme_label' => $themes[$theme], 'theme_label' => $themes[$theme],
'count_data' => json_encode($countData), 'count_data' => $countData,
'completion_data' => json_encode($completionData), 'completion_data' => $completionData,
'current_count' => $currentCount,
'current_completion' => $currentCompletion,
'icons' => \App\Service\FollowUpService::getFollowUpIcons(), 'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'followup_labels' => $themes, 'followup_labels' => $themes,
'geojson' => json_encode($geojson), 'geojson' => json_encode($geojson),

View file

@ -5,7 +5,7 @@
{% block stylesheets %} {% block stylesheets %}
{{ parent() }} {{ parent() }}
<link href='{{ asset('js/maplibre/maplibre-gl.css') }}' rel='stylesheet'/> <link href='{{ asset('js/maplibre/maplibre-gl.css') }}' rel='stylesheet'/>
<link href='{{ asset('css/city-sidebar.css') }}' rel='stylesheet' /> <link href='{{ asset('css/city-sidebar.css') }}' rel='stylesheet'/>
<style> <style>
.chart-container { .chart-container {
width: 100%; width: 100%;
@ -96,28 +96,46 @@
border-radius: 8px; border-radius: 8px;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.maplibregl-popup-content { .maplibregl-popup-content {
font-size: 0.95em; font-size: 0.95em;
} }
.btn-josm { .btn-josm {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
/* Bouton flottant suggestion desktop */ /* Bouton flottant suggestion desktop */
.suggestion-float-btn { .suggestion-float-btn {
position: fixed; position: fixed;
bottom: 32px; bottom: 32px;
right: 32px; right: 32px;
z-index: 1000; z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
display: none; display: none;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.suggestion-float-btn { display: block; } .suggestion-float-btn {
.suggestion-footer-btn { display: none !important; } display: block;
}
.suggestion-footer-btn {
display: none !important;
}
} }
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.suggestion-float-btn { display: none !important; } .suggestion-float-btn {
.suggestion-footer-btn { display: block; width: 100%; margin: 0; border-radius: 0; } display: none !important;
}
.suggestion-footer-btn {
display: block;
width: 100%;
margin: 0;
border-radius: 0;
}
} }
</style> </style>
{% endblock %} {% endblock %}
@ -135,178 +153,176 @@
<div class="p-4"> <div class="p-4">
{# DEBUG : Affichage des objets Place trouvés pour cette ville #} {# DEBUG : Affichage des objets Place trouvés pour cette ville #}
{% if places is defined %} {% if places is defined %}
<div class="alert alert-warning" style="font-size:0.95em;"> <div class="alert alert-warning" style="font-size:0.95em;">
<b>DEBUG : Objets Place trouvés pour cette ville (avant filtrage)</b><br> <b>DEBUG : Objets Place trouvés pour cette ville (avant filtrage)</b><br>
<table class="table table-sm table-bordered mt-2 mb-0"> <table class="table table-sm table-bordered mt-2 mb-0">
<thead><tr> <thead>
<th>#</th><th>id</th><th>main_tag</th><th>osm_kind</th><th>nom</th><th>lat</th><th>lon</th> <tr>
</tr></thead> <th>#</th>
<tbody> <th>id</th>
{% for p in places %} <th>main_tag</th>
<tr> <th>osm_kind</th>
<td>{{ loop.index }}</td> <th>nom</th>
<td>{{ p.getOsmId() }}</td> <th>lat</th>
<td>{{ p.getMainTag() }}</td> <th>lon</th>
<td>{{ p.getOsmKind() }}</td> </tr>
<td>{{ p.getName() }}</td> </thead>
<td>{{ p.getLat() }}</td> <tbody>
<td>{{ p.getLon() }}</td> {% for p in places %}
</tr> <tr>
{% endfor %} <td>{{ loop.index }}</td>
</tbody> <td>{{ p.getOsmId() }}</td>
</table> <td>{{ p.getMainTag() }}</td>
</div> <td>{{ p.getOsmKind() }}</td>
{% endif %} <td>{{ p.getName() }}</td>
<div class="stats-header"> <td>{{ p.getLat() }}</td>
<div class="d-flex justify-content-between align-items-start"> <td>{{ p.getLon() }}</td>
<div> </tr>
<h2> {% endfor %}
<i class="bi {{ icons[theme]|default('bi-question-circle') }}"></i> </tbody>
{{ theme_label }} - {{ stats.name }} </table>
</h2> </div>
<p class="mb-0">Code INSEE: {{ stats.zone }}</p> {% endif %}
</div> <div class="stats-header">
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-outline-secondary"> <div class="d-flex justify-content-between align-items-start">
<i class="bi bi-arrow-left"></i> Retour aux stats <div>
</a> <h2>
</div> <i class="bi {{ icons[theme]|default('bi-question-circle') }}"></i>
</div> {{ theme_label }} - {{ stats.name }}
</h2>
<div class="action-bar"> <p class="mb-0">Code INSEE: {{ stats.zone }}</p>
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-primary"> </div>
<i class="bi bi-bar-chart"></i> Stats de la ville <a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}"
</a> class="btn btn-outline-secondary">
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-warning"> <i class="bi bi-arrow-left"></i> Retour aux stats
<i class="bi bi-arrow-clockwise"></i> Labourer la ville </a>
</a> </div>
<a href="{{ path('admin_followup_graph', {'insee_code': stats.zone}) }}" class="btn btn-info"> </div>
<i class="bi bi-graph-up"></i> Suivi OSM (graphes)
</a>
<a href="{{ path('admin_followup_embed_graph', {'insee_code': stats.zone, 'theme': theme}) }}" class="btn btn-success" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Graphe embedded
</a>
<a href="{{ path('app_admin') }}" class="btn btn-secondary">
<i class="bi bi-house"></i> Accueil admin
</a>
<a href="{{ path('app_public_stats_evolutions', {'insee_code': stats.zone}) }}" class="btn btn-outline-primary">
<i class="bi bi-clock-history"></i> Évolutions temporelles
</a>
<a href="{{ path('admin_street_completion', {'insee_code': stats.zone}) }}" class="btn btn-outline-success">
<i class="bi bi-signpost"></i> Complétion des rues
</a>
</div>
{% if theme == 'bicycle_parking' %}
{% include 'admin/_followup_bicycle_parking_extra.html.twig' %}
{% endif %}
{% if theme == 'camera' %}
{% include 'admin/_followup_cameras_extra.html.twig' %}
{% endif %}
{% if overpass_query is defined %} {% if theme == 'bicycle_parking' %}
<a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank" class="btn btn-outline-primary"> {% include 'admin/_followup_bicycle_parking_extra.html.twig' %}
<i class="bi bi-geo"></i> Vérifier sur Overpass Turbo {% endif %}
</a>
{% endif %}
{% if josm_url %}
<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
</a>
{% else %}
<div class="alert alert-info mb-3">Aucun objet sélectionné pour ce thème, rien à charger dans JOSM.</div>
{% endif %}
<div id="themeMap"></div> {% if theme == 'camera' %}
{% include 'admin/_followup_cameras_extra.html.twig' %}
<div class="stats-grid"> {% endif %}
<div class="stat-card">
<div class="stat-value" id="currentCount">-</div>
<div class="stat-label">Nombre actuel</div>
</div>
<div class="stat-card">
<div class="stat-value" id="currentCompletion">-</div>
<div class="stat-label">Complétion actuelle</div>
</div>
<div class="stat-card">
<div class="stat-value" id="dataPoints">-</div>
<div class="stat-label">Points de données</div>
</div>
<div class="stat-card">
<div class="stat-value" id="lastUpdate">-</div>
<div class="stat-label">Dernière mise à jour</div>
</div>
</div>
<div class="chart-container">
<canvas id="themeChart"></canvas>
</div>
{% if completion_tags is defined and completion_tags[theme] is defined %} {% if josm_url %}
<div class="card mt-4"> <a href="{{ josm_url }}" class="btn btn-outline-dark btn-josm" target="_blank">
<div class="card-header"> <i class="bi bi-box-arrow-up-right"></i> Ouvrir tous les objets dans JOSM
<i class="bi bi-info-circle"></i> Critères de complétion attendus pour ce thème
</div>
<div class="card-body p-2">
<ul class="mb-0">
{% for tag in completion_tags[theme] %}
<li><code>{{ tag }}</code></li>
{% else %}
<li><span class="text-muted">Aucun critère défini</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
<div class="card mt-5">
<div class="card-header">
<i class="bi bi-tags"></i> Statistiques des tags utilisés dans les objets trouvés
</div>
<div class="card-body p-2">
<div id="tags-stats-block">
<table class="table table-sm table-bordered mb-0" id="tags-stats-table" style="max-width:600px;">
<thead>
<tr>
<th>Tag</th>
<th>Nombre d'occurrences</th>
</tr>
</thead>
<tbody>
<tr><td colspan="2" class="text-muted">Chargement...</td></tr>
</tbody>
</table>
</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> </a>
</li> {% else %}
{% endif %} <div class="alert alert-info mb-3">Aucun objet sélectionné pour ce thème, rien à charger dans
{% endfor %} JOSM.
</ul> </div>
</div> {% endif %}
{% endif %} {% if overpass_query is defined %}
</div> <a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank"
<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"> class="btn btn-outline-primary">
<i class="bi bi-chat-dots"></i> Faire une suggestion <i class="bi bi-geo"></i> Overpass Turbo
</a> </a>
{% endif %}
<div id="themeMap"></div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="currentCount">{{ current_count }}</div>
<div class="stat-label">Nombre actuel</div>
</div>
<div class="stat-card">
<div class="stat-value" id="currentCompletion">{{ current_completion }}</div>
<div class="stat-label">Complétion actuelle</div>
</div>
{# <div class="stat-card"> #}
{# <div class="stat-value" id="dataPoints">{{ count_data|length }}</div> #}
{# <div class="stat-label">Points de données</div> #}
{# </div> #}
{# <div class="stat-card"> #}
{# <div class="stat-value" id="lastUpdate">{{ last_update }}</div> #}
{# <div class="stat-label">Dernière mise à jour</div> #}
{# </div> #}
</div>
<div class="chart-container">
<canvas id="themeChart"></canvas>
</div>
{% if completion_tags is defined %}
completion_tags
{{ dump(completion_tags[theme]) }}
{% endif %}
{% if completion_tags is defined and completion_tags[theme] is defined %}
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-info-circle"></i> Critères de complétion attendus pour ce thème
</div>
<div class="card-body p-2">
<ul class="mb-0">
{% for tag in completion_tags[theme] %}
<li><code>{{ tag }}</code></li>
{% else %}
<li><span class="text-muted">Aucun critère défini</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div> </div>
<div class="card mt-5">
<div class="card-header">
<i class="bi bi-tags"></i> Statistiques des tags utilisés dans les objets trouvés
</div>
<div class="card-body p-2">
<div id="tags-stats-block">
<table class="table table-sm table-bordered mb-0" id="tags-stats-table"
style="max-width:600px;">
<thead>
<tr>
<th>Tag</th>
<th>Nombre d'occurrences</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2" class="text-muted">Chargement...</td>
</tr>
</tbody>
</table>
</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>
<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
</a>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
@ -328,7 +344,7 @@
const IGN_RASTER_ID = 'ign-ortho'; const IGN_RASTER_ID = 'ign-ortho';
const IGN_LAYER_ID = 'ign-ortho-layer'; const IGN_LAYER_ID = 'ign-ortho-layer';
const IGN_RASTER_URL = 'https://wxs.ign.fr/ortho/geoportail/wmts?layer=ORTHOIMAGERY.ORTHOPHOTOS&style=normal&tilematrixset=PM&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/jpeg&TileMatrix={z}&TileCol={x}&TileRow={y}'; const IGN_RASTER_URL = 'https://wxs.ign.fr/ortho/geoportail/wmts?layer=ORTHOIMAGERY.ORTHOPHOTOS&style=normal&tilematrixset=PM&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/jpeg&TileMatrix={z}&TileCol={x}&TileRow={y}';
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
if (!overpassQuery) { if (!overpassQuery) {
document.getElementById('themeMap').innerHTML = '<div class="alert alert-warning">Aucune requête Overpass disponible pour ce thème.</div>'; document.getElementById('themeMap').innerHTML = '<div class="alert alert-warning">Aucune requête Overpass disponible pour ce thème.</div>';
return; return;
@ -344,7 +360,7 @@
// Gestion du changement de fond de carte // Gestion du changement de fond de carte
const basemapSelect = document.getElementById('basemapSelect'); const basemapSelect = document.getElementById('basemapSelect');
if (basemapSelect) { if (basemapSelect) {
basemapSelect.addEventListener('change', function() { basemapSelect.addEventListener('change', function () {
const val = basemapSelect.value; const val = basemapSelect.value;
if (val === 'streets') { if (val === 'streets') {
mapInstance.setStyle(basemaps['streets']); mapInstance.setStyle(basemaps['streets']);
@ -377,77 +393,87 @@
fetch('https://overpass-api.de/api/interpreter', { fetch('https://overpass-api.de/api/interpreter', {
method: 'POST', method: 'POST',
body: overpassQuery, body: overpassQuery,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' } headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (!data.elements || data.elements.length === 0) { if (!data.elements || data.elements.length === 0) {
document.getElementById('themeMap').innerHTML = '<div class="alert alert-warning">Aucun objet trouvé via Overpass pour ce thème.</div>'; document.getElementById('themeMap').innerHTML = '<div class="alert alert-warning">Aucun objet trouvé via Overpass pour ce thème.</div>';
// Vider le tableau des tags // Vider le tableau des tags
document.querySelector('#tags-stats-table tbody').innerHTML = '<tr><td colspan="2" class="text-muted">Aucun objet trouvé</td></tr>'; document.querySelector('#tags-stats-table tbody').innerHTML = '<tr><td colspan="2" class="text-muted">Aucun objet trouvé</td></tr>';
return; return;
}
// Centrage carte
let lats = [], lons = [];
data.elements.forEach(e => {
if (e.lat && e.lon) {
lats.push(e.lat); lons.push(e.lon);
} else if (e.type === 'way' && e.center) {
lats.push(e.center.lat); lons.push(e.center.lon);
} }
}); // Centrage carte
if (lats.length && lons.length) { let lats = [], lons = [];
const avgLat = lats.reduce((a,b)=>a+b,0)/lats.length; let completions_list = [];
const avgLon = lons.reduce((a,b)=>a+b,0)/lons.length; data.elements.forEach(e => {
mapInstance.setCenter([avgLon, avgLat]); if (e.lat && e.lon) {
} lats.push(e.lat);
// Marqueurs lons.push(e.lon);
data.elements.forEach(e => { } else if (e.type === 'way' && e.center) {
let lat = null, lon = null; lats.push(e.center.lat);
if (e.type === 'node') { lons.push(e.center.lon);
lat = e.lat; lon = e.lon; }
} else if (e.center) { });
lat = e.center.lat; lon = e.center.lon; if (lats.length && lons.length) {
const avgLat = lats.reduce((a, b) => a + b, 0) / lats.length;
const avgLon = lons.reduce((a, b) => a + b, 0) / lons.length;
mapInstance.setCenter([avgLon, avgLat]);
} }
if (!lat || !lon) return; // On ignore les ways sans centroïde // Marqueurs
// Calcul de la complétion data.elements.forEach(e => {
let filled = 0; let lat = null, lon = null;
let missingTags = []; if (e.type === 'node') {
if (completionTags && completionTags.length > 0) { lat = e.lat;
completionTags.forEach(tag => { lon = e.lon;
if (e.tags && typeof e.tags[tag] !== 'undefined' && e.tags[tag] !== null && e.tags[tag] !== '') { } else if (e.center) {
filled++; lat = e.center.lat;
} else { lon = e.center.lon;
missingTags.push(tag); }
} if (!lat || !lon) return; // On ignore les ways sans centroïde
}); // Calcul de la complétion
} let filled = 0;
let completion = completionTags && completionTags.length > 0 ? Math.round(100 * filled / completionTags.length) : null; let missingTags = [];
// Couleur dégradée du gris au vert intense if (completionTags && completionTags.length > 0) {
function lerpColor(a, b, t) { completionTags.forEach(tag => {
// a et b sont des couleurs hex, t entre 0 et 1 if (e.tags && typeof e.tags[tag] !== 'undefined' && e.tags[tag] !== null && e.tags[tag] !== '') {
const ah = a.replace('#', ''); filled++;
const bh = b.replace('#', ''); } else {
const ar = parseInt(ah.substring(0,2), 16), ag = parseInt(ah.substring(2,4), 16), ab = parseInt(ah.substring(4,6), 16); missingTags.push(tag);
const br = parseInt(bh.substring(0,2), 16), bg = parseInt(bh.substring(2,4), 16), bb = parseInt(bh.substring(4,6), 16); }
const rr = Math.round(ar + (br-ar)*t); });
const rg = Math.round(ag + (bg-ag)*t); }
const rb = Math.round(ab + (bb-ab)*t); let completion = completionTags && completionTags.length > 0 ? Math.round(100 * filled / completionTags.length) : null;
return '#' + rr.toString(16).padStart(2,'0') + rg.toString(16).padStart(2,'0') + rb.toString(16).padStart(2,'0'); completions_list.push(completion);
}
let color = '#cccccc'; // gris par défaut // Couleur dégradée du gris au vert intense
if (completion !== null) { function lerpColor(a, b, t) {
color = lerpColor('#cccccc', '#008000', Math.max(0, Math.min(1, completion/100))); // a et b sont des couleurs hex, t entre 0 et 1
} const ah = a.replace('#', '');
// Affichage des tags manquants const bh = b.replace('#', '');
let missingHtml = ''; const ar = parseInt(ah.substring(0, 2), 16), ag = parseInt(ah.substring(2, 4), 16),
if (missingTags.length > 0) { ab = parseInt(ah.substring(4, 6), 16);
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>`; const br = parseInt(bh.substring(0, 2), 16), bg = parseInt(bh.substring(2, 4), 16),
} bb = parseInt(bh.substring(4, 6), 16);
// Liens édition JOSM et iD const rr = Math.round(ar + (br - ar) * t);
const josmUrl = `http://127.0.0.1:8111/load_object?objects=${e.type[0].toUpperCase()}${e.id}`; const rg = Math.round(ag + (bg - ag) * t);
const idUrl = `https://www.openstreetmap.org/edit?editor=id&${e.type}=${e.id}`; const rb = Math.round(ab + (bb - ab) * t);
const popupHtml = `<div style='min-width:180px'> return '#' + rr.toString(16).padStart(2, '0') + rg.toString(16).padStart(2, '0') + rb.toString(16).padStart(2, '0');
}
let color = '#cccccc'; // gris par défaut
if (completion !== null) {
color = lerpColor('#cccccc', '#008000', Math.max(0, Math.min(1, completion / 100)));
}
// Affichage des tags manquants
let missingHtml = '';
if (missingTags.length > 0) {
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'>
<h2 class="title is-2">${e.tags && e.tags.name ? e.tags.name : '(sans nom)'}</h2><br> <h2 class="title is-2">${e.tags && e.tags.name ? e.tags.name : '(sans nom)'}</h2><br>
<b>Complétion :</b> ${completion !== null ? completion + '%' : ''} <b>Complétion :</b> ${completion !== null ? completion + '%' : ''}
${missingHtml} ${missingHtml}
@ -461,37 +487,51 @@
<a class="btn btn-info" href='${idUrl}' target='_blank'><i class="bi bi-pencil" ></i>iD</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> <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])
.setPopup(new maplibregl.Popup({ offset: 18 }).setHTML(popupHtml)) .setPopup(new maplibregl.Popup({offset: 18}).setHTML(popupHtml))
.addTo(mapInstance); .addTo(mapInstance);
}); });
// --- Statistiques des tags --- // --- Statistiques des tags ---
const tagCounts = {}; const tagCounts = {};
data.elements.forEach(e => { data.elements.forEach(e => {
if (e.tags) { if (e.tags) {
Object.entries(e.tags).forEach(([k, v]) => { Object.entries(e.tags).forEach(([k, v]) => {
if (!tagCounts[k]) tagCounts[k] = 0; if (!tagCounts[k]) tagCounts[k] = 0;
tagCounts[k]++; tagCounts[k]++;
}); });
}
});
const average_completion = completions_list.reduce((a, b) => a + b, 0) / completions_list.length;
const count_objects = data.elements.length;
const current_count = document.querySelector('#currentCount');
const current_completion = document.querySelector('#currentCompletion');
if (current_count && count_objects) {
current_count.textContent = count_objects;
} }
if (average_completion && current_completion) {
current_completion.textContent = average_completion.toFixed(2) + ' %';
}
const tbody = document.querySelector('#tags-stats-table tbody');
if (Object.keys(tagCounts).length === 0) {
tbody.innerHTML = '<tr><td colspan="2" class="text-muted">Aucun tag trouvé</td></tr>';
} else {
tbody.innerHTML = Object.entries(tagCounts)
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => `<tr><td><code>${k}</code></td><td>${v}</td></tr>`)
.join('');
}
})
.catch(err => {
document.getElementById('themeMap').innerHTML = '<div class="alert alert-danger">Erreur lors de la requête Overpass : ' + err + '</div>';
}); });
const tbody = document.querySelector('#tags-stats-table tbody');
if (Object.keys(tagCounts).length === 0) {
tbody.innerHTML = '<tr><td colspan="2" class="text-muted">Aucun tag trouvé</td></tr>';
} else {
tbody.innerHTML = Object.entries(tagCounts)
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => `<tr><td><code>${k}</code></td><td>${v}</td></tr>`)
.join('');
}
})
.catch(err => {
document.getElementById('themeMap').innerHTML = '<div class="alert alert-danger">Erreur lors de la requête Overpass : ' + err + '</div>';
});
}); });
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@ -501,26 +541,42 @@
const countData = {{ count_data|json_encode|raw }}; const countData = {{ count_data|json_encode|raw }};
console.log(countData) console.log('Count data:', countData);
const completionData = {{ completion_data|json_encode|raw }}; const completionData = {{ completion_data|json_encode|raw }};
console.log('Completion data:', completionData);
// Current metrics from server
const currentCount = {{ current_count }};
const currentCompletion = {{ current_completion }};
// Mettre à jour les statistiques // Mettre à jour les statistiques
function updateStats() { function updateStats() {
if (Array.isArray(countData) && countData.length > 0) { // Use current metrics from server if available
if (typeof currentCount !== 'undefined') {
document.getElementById('currentCount').textContent = currentCount;
} else if (Array.isArray(countData) && countData.length > 0) {
const latestCount = countData[countData.length - 1]; const latestCount = countData[countData.length - 1];
document.getElementById('currentCount').textContent = latestCount.value; document.getElementById('currentCount').textContent = latestCount.value;
document.getElementById('lastUpdate').textContent = new Date(latestCount.date).toLocaleDateString('fr-FR');
} }
if (Array.isArray(completionData) && completionData.length > 0) { if (typeof currentCompletion !== 'undefined') {
document.getElementById('currentCompletion').textContent = currentCompletion + '%';
} else if (Array.isArray(completionData) && completionData.length > 0) {
const latestCompletion = completionData[completionData.length - 1]; const latestCompletion = completionData[completionData.length - 1];
document.getElementById('currentCompletion').textContent = latestCompletion.value + '%'; document.getElementById('currentCompletion').textContent = latestCompletion.value + '%';
} }
// Set last update date
if (Array.isArray(countData) && countData.length > 0) {
const latestCount = countData[countData.length - 1];
document.getElementById('lastUpdate').textContent = new Date(latestCount.date).toLocaleDateString('fr-FR');
} else {
document.getElementById('lastUpdate').textContent = new Date().toLocaleDateString('fr-FR');
}
document.getElementById('dataPoints').textContent = Math.max( document.getElementById('dataPoints').textContent = Math.max(
Array.isArray(countData) ? countData.length : 0, Array.isArray(countData) ? countData.length : 0,
Array.isArray(completionData) ? completionData.length : 0 Array.isArray(completionData) ? completionData.length : 0
); );
} }
@ -574,57 +630,159 @@
datasets: [ datasets: [
{ {
label: "Nombre d'objets", label: "Nombre d'objets",
data: Array.isArray(countData) ? countData.map(d => ({ x: new Date(d.date), y: d.value })) : [], data: Array.isArray(countData) ? countData.map(d => ({x: new Date(d.date), y: d.value})) : [],
borderColor: '#0d6efd', borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)', backgroundColor: 'rgba(13, 110, 253, 0.1)',
borderWidth: 2, borderWidth: 2,
fill: true, fill: true,
tension: 0.1, tension: 0.1,
yAxisID: 'y1', yAxisID: 'y1',
},
{
label: 'Pourcentage de complétion',
data: Array.isArray(completionData) ? completionData.map(d => ({ x: new Date(d.date), y: d.value })) : [],
borderColor: '#198754',
backgroundColor: 'rgba(25, 135, 84, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.1,
yAxisID: 'y2',
} }
] }
}, }
options: { }
responsive: true, }
maintainAspectRatio: false, },
plugins: { {
legend: { display: true }, label: 'Pourcentage de complétion',
tooltip: { mode: 'index', intersect: false } data
}, :
interaction: { mode: 'nearest', axis: 'x', intersect: false }, Array.isArray(completionData) ? completionData.map(d => ({x: new Date(d.date), y: d.value})) : [],
scales: { borderColor
x: { :
type: 'time', '#198754',
time: { unit: 'day', displayFormats: { day: 'dd/MM/yyyy' } }, backgroundColor
title: { display: true, text: 'Date' } :
}, 'rgba(25, 135, 84, 0.1)',
y1: { borderWidth
type: 'linear', :
position: 'left', 2,
title: { display: true, text: "Nombre d'objets" }, fill
beginAtZero: true :
}, true,
y2: { tension
type: 'linear', :
position: 'right', 0.1,
title: { display: true, text: 'Complétion (%)' }, yAxisID
min: 0, :
max: 100, 'y2',
grid: { drawOnChartArea: false } }
,
// Add current data point if no historical data exists
...
((!Array.isArray(countData) || countData.length === 0) && typeof currentCount !== 'undefined' ? [{
label: "Nombre actuel",
data: [{x: new Date(), y: currentCount}],
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
borderWidth: 2,
pointRadius: 5,
fill: false,
yAxisID: 'y1',
}] : []),
...
((!Array.isArray(completionData) || completionData.length === 0) && typeof currentCompletion !== 'undefined' ? [{
label: "Complétion actuelle",
data: [{x: new Date(), y: currentCompletion}],
borderColor: '#fd7e14',
backgroundColor: 'rgba(253, 126, 20, 0.1)',
borderWidth: 2,
pointRadius: 5,
fill: false,
yAxisID: 'y2',
}] : [])
]
},
options: {
responsive: true,
maintainAspectRatio
:
false,
plugins
:
{
legend: {
display: true
}
,
tooltip: {
mode: 'index', intersect
:
false
}
}
,
interaction: {
mode: 'nearest', axis
:
'x', intersect
:
false
}
,
scales: {
x: {
type: 'time',
time
:
{
unit: 'day', displayFormats
:
{
day: 'dd/MM/yyyy'
}
}
,
title: {
display: true, text
:
'Date'
}
}
,
y1: {
type: 'linear',
position
:
'left',
title
:
{
display: true, text
:
"Nombre d'objets"
}
,
beginAtZero: true
}
,
y2: {
type: 'linear',
position
:
'right',
title
:
{
display: true, text
:
'Complétion (%)'
}
,
min: 0,
max
:
100,
grid
:
{
drawOnChartArea: false
} }
} }
} }
}); }
})
;
// Initialiser les statistiques // Initialiser les statistiques
updateStats(); updateStats();