From af1233c2461bfc6fbdd169c699476d8fee87eeb5 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Tue, 12 Aug 2025 11:23:20 +0200 Subject: [PATCH] =?UTF-8?q?page=20de=20d=C3=A9tail,=20ajout=20de=20mesures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/AdminController.php | 56 +- .../admin/followup_theme_graph.html.twig | 780 +++++++++++------- 2 files changed, 523 insertions(+), 313 deletions(-) diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 2e57ac58..51ce684d 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -733,12 +733,64 @@ final class AdminController extends AbstractController $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', [ 'stats' => $stats, 'theme' => $theme, 'theme_label' => $themes[$theme], - 'count_data' => json_encode($countData), - 'completion_data' => json_encode($completionData), + 'count_data' => $countData, + 'completion_data' => $completionData, + 'current_count' => $currentCount, + 'current_completion' => $currentCompletion, 'icons' => \App\Service\FollowUpService::getFollowUpIcons(), 'followup_labels' => $themes, 'geojson' => json_encode($geojson), diff --git a/templates/admin/followup_theme_graph.html.twig b/templates/admin/followup_theme_graph.html.twig index 7f46d940..5e782bc6 100644 --- a/templates/admin/followup_theme_graph.html.twig +++ b/templates/admin/followup_theme_graph.html.twig @@ -5,7 +5,7 @@ {% block stylesheets %} {{ parent() }} - + {% endblock %} @@ -135,178 +153,176 @@
{# DEBUG : Affichage des objets Place trouvés pour cette ville #} {% if places is defined %} -
- DEBUG : Objets Place trouvés pour cette ville (avant filtrage)
- - - - - - {% for p in places %} - - - - - - - - - - {% endfor %} - -
#idmain_tagosm_kindnomlatlon
{{ loop.index }}{{ p.getOsmId() }}{{ p.getMainTag() }}{{ p.getOsmKind() }}{{ p.getName() }}{{ p.getLat() }}{{ p.getLon() }}
-
- {% endif %} -
-
-
-

- - {{ theme_label }} - {{ stats.name }} -

-

Code INSEE: {{ stats.zone }}

-
- - Retour aux stats - -
-
- - - -{% if theme == 'bicycle_parking' %} - {% include 'admin/_followup_bicycle_parking_extra.html.twig' %} -{% endif %} - -{% if theme == 'camera' %} - {% include 'admin/_followup_cameras_extra.html.twig' %} -{% endif %} +
+ DEBUG : Objets Place trouvés pour cette ville (avant filtrage)
+ + + + + + + + + + + + + + {% for p in places %} + + + + + + + + + + {% endfor %} + +
#idmain_tagosm_kindnomlatlon
{{ loop.index }}{{ p.getOsmId() }}{{ p.getMainTag() }}{{ p.getOsmKind() }}{{ p.getName() }}{{ p.getLat() }}{{ p.getLon() }}
+
+ {% endif %} +
+
+
+

+ + {{ theme_label }} - {{ stats.name }} +

+

Code INSEE: {{ stats.zone }}

+
+ + Retour aux stats + +
+
- {% if overpass_query is defined %} - - Vérifier sur Overpass Turbo - - {% endif %} - {% if josm_url %} - - Ouvrir tous les objets dans JOSM - - {% else %} -
Aucun objet sélectionné pour ce thème, rien à charger dans JOSM.
- {% endif %} + {% if theme == 'bicycle_parking' %} + {% include 'admin/_followup_bicycle_parking_extra.html.twig' %} + {% endif %} -
- -
-
-
-
-
Nombre actuel
-
-
-
-
-
Complétion actuelle
-
-
-
-
-
Points de données
-
-
-
-
-
Dernière mise à jour
-
-
+ {% if theme == 'camera' %} + {% include 'admin/_followup_cameras_extra.html.twig' %} + {% endif %} -
- -
- {% if completion_tags is defined and completion_tags[theme] is defined %} -
-
- Critères de complétion attendus pour ce thème -
-
-
    - {% for tag in completion_tags[theme] %} -
  • {{ tag }}
  • - {% else %} -
  • Aucun critère défini
  • - {% endfor %} -
-
-
- {% endif %} -
-
-
- Statistiques des tags utilisés dans les objets trouvés -
-
-
- - - - - - - - - - -
TagNombre d'occurrences
Chargement...
-
-
-{# Bloc navigation autres thématiques #} -{% if followup_labels is defined and icons is defined %} -
-
-

Autres thématiques de suivi :

- -
-{% endif %} -
- - Faire une suggestion - + {% else %} +
Aucun objet sélectionné pour ce thème, rien à charger dans + JOSM. +
+ {% endif %} + {% if overpass_query is defined %} + + Overpass Turbo + + {% endif %} + +
+ +
+
+
{{ current_count }}
+
Nombre actuel
+
+
+
{{ current_completion }}
+
Complétion actuelle
+
+ {#
#} + {#
{{ count_data|length }}
#} + {#
Points de données
#} + {#
#} + {#
#} + {#
{{ last_update }}
#} + {#
Dernière mise à jour
#} + {#
#} +
+ + +
+ +
+ {% if completion_tags is defined %} + completion_tags + {{ dump(completion_tags[theme]) }} + + {% endif %} + {% if completion_tags is defined and completion_tags[theme] is defined %} +
+
+ Critères de complétion attendus pour ce thème +
+
+
    + {% for tag in completion_tags[theme] %} +
  • {{ tag }}
  • + {% else %} +
  • Aucun critère défini
  • + {% endfor %} +
+
+
+ {% endif %} +
+
+ Statistiques des tags utilisés dans les objets trouvés +
+
+
+ + + + + + + + + + + + +
TagNombre d'occurrences
Chargement...
+
+
+ {# Bloc navigation autres thématiques #} + {% if followup_labels is defined and icons is defined %} +
+
+

Autres thématiques de suivi :

+
    + {% for t, label in followup_labels %} + {% if t != theme %} +
  • + + {{ label }} + +
  • + {% endif %} + {% endfor %} +
+
+ {% endif %} +
+ + Faire une suggestion + + {% endblock %} @@ -328,7 +344,7 @@ const IGN_RASTER_ID = 'ign-ortho'; 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}'; - document.addEventListener('DOMContentLoaded', function() { + document.addEventListener('DOMContentLoaded', function () { if (!overpassQuery) { document.getElementById('themeMap').innerHTML = '
Aucune requête Overpass disponible pour ce thème.
'; return; @@ -344,7 +360,7 @@ // Gestion du changement de fond de carte const basemapSelect = document.getElementById('basemapSelect'); if (basemapSelect) { - basemapSelect.addEventListener('change', function() { + basemapSelect.addEventListener('change', function () { const val = basemapSelect.value; if (val === 'streets') { mapInstance.setStyle(basemaps['streets']); @@ -377,77 +393,87 @@ fetch('https://overpass-api.de/api/interpreter', { method: 'POST', body: overpassQuery, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + headers: {'Content-Type': 'application/x-www-form-urlencoded'} }) - .then(response => response.json()) - .then(data => { - if (!data.elements || data.elements.length === 0) { - document.getElementById('themeMap').innerHTML = '
Aucun objet trouvé via Overpass pour ce thème.
'; - // Vider le tableau des tags - document.querySelector('#tags-stats-table tbody').innerHTML = 'Aucun objet trouvé'; - 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); + .then(response => response.json()) + .then(data => { + if (!data.elements || data.elements.length === 0) { + document.getElementById('themeMap').innerHTML = '
Aucun objet trouvé via Overpass pour ce thème.
'; + // Vider le tableau des tags + document.querySelector('#tags-stats-table tbody').innerHTML = 'Aucun objet trouvé'; + return; } - }); - 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]); - } - // Marqueurs - data.elements.forEach(e => { - let lat = null, lon = null; - if (e.type === 'node') { - lat = e.lat; lon = e.lon; - } else if (e.center) { - lat = e.center.lat; lon = e.center.lon; + // Centrage carte + let lats = [], lons = []; + let completions_list = []; + 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); + } + }); + 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 - // Calcul de la complétion - let filled = 0; - let missingTags = []; - if (completionTags && completionTags.length > 0) { - completionTags.forEach(tag => { - if (e.tags && typeof e.tags[tag] !== 'undefined' && e.tags[tag] !== null && e.tags[tag] !== '') { - filled++; - } else { - missingTags.push(tag); - } - }); - } - let completion = completionTags && completionTags.length > 0 ? Math.round(100 * filled / completionTags.length) : null; - // Couleur dégradée du gris au vert intense - function lerpColor(a, b, t) { - // a et b sont des couleurs hex, t entre 0 et 1 - const ah = a.replace('#', ''); - const bh = b.replace('#', ''); - const ar = parseInt(ah.substring(0,2), 16), ag = parseInt(ah.substring(2,4), 16), ab = parseInt(ah.substring(4,6), 16); - 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); - 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 = `
Manque : ` + missingTags.map(t => `${t}`).join(', ') + `
`; - } - // 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 = `
+ // Marqueurs + data.elements.forEach(e => { + let lat = null, lon = null; + if (e.type === 'node') { + lat = e.lat; + lon = e.lon; + } else if (e.center) { + lat = e.center.lat; + lon = e.center.lon; + } + if (!lat || !lon) return; // On ignore les ways sans centroïde + // Calcul de la complétion + let filled = 0; + let missingTags = []; + if (completionTags && completionTags.length > 0) { + completionTags.forEach(tag => { + if (e.tags && typeof e.tags[tag] !== 'undefined' && e.tags[tag] !== null && e.tags[tag] !== '') { + filled++; + } else { + missingTags.push(tag); + } + }); + } + let completion = completionTags && completionTags.length > 0 ? Math.round(100 * filled / completionTags.length) : null; + completions_list.push(completion); + + // Couleur dégradée du gris au vert intense + function lerpColor(a, b, t) { + // a et b sont des couleurs hex, t entre 0 et 1 + const ah = a.replace('#', ''); + const bh = b.replace('#', ''); + const ar = parseInt(ah.substring(0, 2), 16), ag = parseInt(ah.substring(2, 4), 16), + ab = parseInt(ah.substring(4, 6), 16); + 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); + 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 = `
Manque : ` + missingTags.map(t => `${t}`).join(', ') + `
`; + } + // 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 = `

${e.tags && e.tags.name ? e.tags.name : '(sans nom)'}


Complétion : ${completion !== null ? completion + '%' : '–'} ${missingHtml} @@ -461,37 +487,51 @@ iD - ${e.tags ? Object.entries(e.tags).map(([k,v]) => `${k}: ${v}`).join('
') : ''}

+ ${e.tags ? Object.entries(e.tags).map(([k, v]) => `${k}: ${v}`).join('
') : ''}

`; - new maplibregl.Marker({ color: color }) - .setLngLat([lon, lat]) - .setPopup(new maplibregl.Popup({ offset: 18 }).setHTML(popupHtml)) - .addTo(mapInstance); - }); - // --- Statistiques des tags --- - const tagCounts = {}; - data.elements.forEach(e => { - if (e.tags) { - Object.entries(e.tags).forEach(([k, v]) => { - if (!tagCounts[k]) tagCounts[k] = 0; - tagCounts[k]++; - }); + new maplibregl.Marker({color: color}) + .setLngLat([lon, lat]) + .setPopup(new maplibregl.Popup({offset: 18}).setHTML(popupHtml)) + .addTo(mapInstance); + }); + // --- Statistiques des tags --- + const tagCounts = {}; + data.elements.forEach(e => { + if (e.tags) { + Object.entries(e.tags).forEach(([k, v]) => { + if (!tagCounts[k]) tagCounts[k] = 0; + 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 = 'Aucun tag trouvé'; + } else { + tbody.innerHTML = Object.entries(tagCounts) + .sort((a, b) => b[1] - a[1]) + .map(([k, v]) => `${k}${v}`) + .join(''); + } + }) + .catch(err => { + document.getElementById('themeMap').innerHTML = '
Erreur lors de la requête Overpass : ' + err + '
'; }); - const tbody = document.querySelector('#tags-stats-table tbody'); - if (Object.keys(tagCounts).length === 0) { - tbody.innerHTML = 'Aucun tag trouvé'; - } else { - tbody.innerHTML = Object.entries(tagCounts) - .sort((a, b) => b[1] - a[1]) - .map(([k, v]) => `${k}${v}`) - .join(''); - } - }) - .catch(err => { - document.getElementById('themeMap').innerHTML = '
Erreur lors de la requête Overpass : ' + err + '
'; - }); }); @@ -501,26 +541,42 @@ const countData = {{ count_data|json_encode|raw }}; - console.log(countData) - + console.log('Count data:', countData); 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 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]; 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]; 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( - Array.isArray(countData) ? countData.length : 0, + Array.isArray(countData) ? countData.length : 0, Array.isArray(completionData) ? completionData.length : 0 ); } @@ -574,57 +630,159 @@ datasets: [ { 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', backgroundColor: 'rgba(13, 110, 253, 0.1)', borderWidth: 2, fill: true, tension: 0.1, 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 }, - 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 } + } + } + } + } + }, + { + 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', + } + , + // 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 updateStats();