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 @@
Aucun objet sélectionné pour ce thème, rien à charger dans
+ JOSM.
+
+ {% endif %}
+ {% if overpass_query is defined %}
+ 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é via Overpass pour ce thème.
';
+ // Vider le tableau des tags
+ document.querySelector('#tags-stats-table tbody').innerHTML = '
+ // 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();