osm-labo/templates/admin/followup_theme_graph.html.twig
2025-08-21 16:50:17 +02:00

988 lines
44 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base_embed.html.twig' %}
{% block title %}Graphique {{ theme_label }} - {{ stats.name }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link href='{{ asset('js/maplibre/maplibre-gl.css') }}' rel='stylesheet'/>
<link href='{{ asset('css/city-sidebar.css') }}' rel='stylesheet'/>
<style>
#alertes_osmose .counter{
background: #8A2BE2;
border-radius: 10em;
margin-right: 1ch;
padding: 0.5rem;
color: white;
}
#themeMap {
margin-top: 1rem;
}
.chart-container {
width: 100%;
height: 400px;
margin: 20px 0;
}
.stats-header {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.action-bar {
background: white;
padding: 15px;
border-radius: 8px;
border: 1px solid #dee2e6;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.action-bar .btn {
white-space: nowrap;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 15px;
border-radius: 8px;
border: 1px solid #dee2e6;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #0d6efd;
}
.stat-label {
font-size: 14px;
color: #6c757d;
margin-top: 5px;
}
.chart-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #dee2e6;
}
.chart-tab {
padding: 10px 20px;
background: none;
border: none;
cursor: pointer;
border-bottom: 3px solid transparent;
}
.chart-tab.active {
border-bottom-color: #0d6efd;
color: #0d6efd;
}
.chart-content {
display: none;
}
.chart-content.active {
display: block;
}
#themeMap {
height: 400px;
width: 100%;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.maplibregl-popup-content {
font-size: 0.95em;
}
.osmose-popup-tag {
margin-bottom: 5px;
}
.osmose-popup-tag-key {
font-weight: bold;
}
.osmose-popup-buttons {
margin-top: 10px;
display: flex;
gap: 5px;
}
/* Bouton flottant suggestion desktop */
.suggestion-float-btn {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
display: none;
}
@media (min-width: 768px) {
.suggestion-float-btn {
display: block;
}
.suggestion-footer-btn {
display: none !important;
}
}
@media (max-width: 767.98px) {
.suggestion-float-btn {
display: none !important;
}
.suggestion-footer-btn {
display: block;
width: 100%;
margin: 0;
border-radius: 0;
}
}
</style>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar de navigation -->
<div class="col-12">
{% include 'admin/_city_sidebar.html.twig' with {'stats': stats, 'active_menu': 'followup_graph'} %}
</div>
<!-- Contenu principal -->
<div class="col-md-9 col-lg-10 main-content">
<div class="p-4">
{# DEBUG : Affichage des objets Place trouvés pour cette ville #}
{% if places is defined %}
<div class="alert alert-warning" style="font-size:0.95em;">
<b>DEBUG : Objets Place trouvés pour cette ville (avant filtrage)</b><br>
<table class="table table-sm table-bordered mt-2 mb-0">
<thead>
<tr>
<th>#</th>
<th>id</th>
<th>main_tag</th>
<th>osm_kind</th>
<th>nom</th>
<th>lat</th>
<th>lon</th>
</tr>
</thead>
<tbody>
{% for p in places %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ p.getOsmId() }}</td>
<td>{{ p.getMainTag() }}</td>
<td>{{ p.getOsmKind() }}</td>
<td>{{ p.getName() }}</td>
<td>{{ p.getLat() }}</td>
<td>{{ p.getLon() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="stats-header">
<div class="d-flex justify-content-between align-items-start">
<div>
<h2>
<i class="bi {{ icons[theme]|default('bi-question-circle') }}"></i>
{{ theme_label }} - {{ stats.name }}
</h2>
<p class="mb-0">Code INSEE: {{ stats.zone }}</p>
</div>
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}"
class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Retour aux stats
</a>
</div>
</div>
{% 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 %}
{% 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> Overpass Turbo
</a>
{% endif %}
<div id="themeMap"></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 %}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="currentCount">
{{ current_count }}
<i class="bi bi-load bi-spin"></i>
...
</div>
<div class="stat-label">Nombre actuel</div>
</div>
<div class="stat-card">
<div class="stat-value" id="currentCompletion">
{{ current_completion }}
<i class="bi bi-load bi-spin"></i>
...
</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 id="alertes_osmose"></div>
<div class="chart-container">
<canvas id="themeChart"></canvas>
</div>
{% 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>
<a href="https://wiki.openstreetmap.org/FR:Key:{{ tag }}">
<code>{{ tag }}</code>
</a>
</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-4">
<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 p-6 m-4">
<h4>Autres thématiques de suivi :</h4>
<ul class="list-inline p-6 m-4">
{% for t, label in followup_labels %}
{% if t != theme %}
<li class="list-inline-item mb-2 ml-4">
<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>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<script>
const overpassQuery = `{% if overpass_query is defined %}{{ overpass_query|replace({
'(._;>;);\nout meta;\n>;': 'out center;'
})|e('js') }}{% endif %}`;
const mapToken = '{{ maptiler_token }}';
// Liste des tags attendus pour la complétion de ce thème
const completionTags = {{ completion_tags[theme]|json_encode|raw }};
// Mapping des thèmes vers les IDs d'items Osmose
const osmoseItemsMapping = {
'charging_station': [8410, 8411],
'school': [8031],
'healthcare': [8211, 7220, 8331],
'laboratory': [7240, 8351],
'police': [8190, 8191],
'defibrillator': [8370]
// Ajouter d'autres thèmes selon les besoins
};
let mapInstance = null;
const basemaps = {
'streets': 'https://api.maptiler.com/maps/streets/style.json?key=' + mapToken,
// 'satellite': 'https://data.geopf.fr/annexes/ressources/vectorTiles/styles/BDORTHO/standard.json' // supprimé car non compatible
};
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 () {
if (!overpassQuery) {
document.getElementById('themeMap').innerHTML = '<div class="alert alert-warning">Aucune requête Overpass disponible pour ce thème.</div>';
return;
}
// Initialiser la carte
mapInstance = new maplibregl.Map({
container: 'themeMap',
style: basemaps['streets'],
center: [2, 48],
zoom: 13
});
mapInstance.addControl(new maplibregl.NavigationControl());
// Charger les analyses Osmose si le thème est supporté
const currentTheme = '{{ theme }}';
if (osmoseItemsMapping[currentTheme]) {
// Récupérer les coordonnées de la commune pour la bounding box
fetch(`https://geo.api.gouv.fr/communes?code={{ stats.zone }}&fields=centre,nom,code,codesPostaux,population,surface,contour`)
.then(response => response.json())
.then(data => {
if (data && data.length > 0) {
const commune = data[0];
if (commune.centre && commune.centre.coordinates) {
// Calculer la bounding box pour la requête Osmose
const lon = commune.centre.coordinates[0];
const lat = commune.centre.coordinates[1];
const offset = 0.05; // Environ 5km
const bbox = [
lon - offset,
lat - offset,
lon + offset,
lat + offset
];
// Charger les analyses Osmose
loadOsmoseAnalyses(mapInstance, currentTheme, bbox);
}
}
})
.catch(error => {
console.error('Erreur lors du chargement des coordonnées de la commune:', error);
});
}
// Gestion du changement de fond de carte
const basemapSelect = document.getElementById('basemapSelect');
if (basemapSelect) {
basemapSelect.addEventListener('change', function () {
const val = basemapSelect.value;
if (val === 'streets') {
mapInstance.setStyle(basemaps['streets']);
} else if (val === 'satellite') {
// Ajout du raster IGN comme dans JOSM
if (!mapInstance.getSource(IGN_RASTER_ID)) {
mapInstance.addSource(IGN_RASTER_ID, {
'type': 'raster',
'tiles': [IGN_RASTER_URL],
'tileSize': 256,
'attribution': 'Données © IGN BD Ortho',
'scheme': 'tms',
'minzoom': 0,
'maxzoom': 19
});
}
if (!mapInstance.getLayer(IGN_LAYER_ID)) {
mapInstance.addLayer({
'id': IGN_LAYER_ID,
'type': 'raster',
'source': IGN_RASTER_ID,
'minzoom': 0,
'maxzoom': 19
});
}
}
});
}
// Requête Overpass
fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: overpassQuery,
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 = '<div class="alert alert-warning">Aucun objet trouvé via Overpass pour ce thème.</div>';
// Vider le tableau des tags
document.querySelector('#tags-stats-table tbody').innerHTML = '<tr><td colspan="2" class="text-muted">Aucun objet trouvé</td></tr>';
return;
}
// 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]);
}
// 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 = `<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>
<b>Complétion :</b> ${completion !== null ? completion + '%' : ''}
${missingHtml}
<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>`;
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) + ' %';
}
// Send measurement to /api/city-followup
const insee_code = '{{ stats.zone }}';
const theme = '{{ theme }}';
// Prepare data for the API request
const measureData = new FormData();
measureData.append('insee_code', insee_code);
measureData.append('measure_label', theme + '_count');
measureData.append('measure_value', count_objects);
// Send count measurement
fetch('/api/city-followup', {
method: 'POST',
body: measureData
})
.then(response => response.json())
.then(result => {
console.log('Count measurement saved:', result);
if (result.success) {
// Add the new measurement to the chart data
const newMeasurement = {
date: result.follow_up.date,
value: result.follow_up.measure
};
// Add to the global countData array
if (Array.isArray(window.countData)) {
window.countData.push(newMeasurement);
// Update the chart
updateChart();
}
}
})
.catch(error => {
console.error('Error saving count measurement:', error);
});
// Send completion measurement
if (average_completion) {
const completionData = new FormData();
completionData.append('insee_code', insee_code);
completionData.append('measure_label', theme + '_completion');
completionData.append('measure_value', average_completion);
fetch('/api/city-followup', {
method: 'POST',
body: completionData
})
.then(response => response.json())
.then(result => {
console.log('Completion measurement saved:', result);
if (result.success) {
// Add the new measurement to the chart data
const newMeasurement = {
date: result.follow_up.date,
value: result.follow_up.measure
};
// Add to the global completionData array
if (Array.isArray(window.completionData)) {
window.completionData.push(newMeasurement);
// Update the chart
updateChart();
}
}
})
.catch(error => {
console.error('Error saving completion measurement:', error);
});
}
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 src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script>
const countData = {{ count_data|json_encode|raw }};
console.log('Count data:', countData);
window.countData = countData;
const completionData = {{ completion_data|json_encode|raw }};
window.completionData = completionData;
console.log('Completion data:', completionData);
// Current metrics from server
const currentCount = {{ current_count }};
const currentCompletion = {{ current_completion }};
// Mettre à jour les statistiques
function updateStats() {
// Use current metrics from server if available
if (document.getElementById('currentCount')) {
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;
}
}
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(completionData) ? completionData.length : 0
);
}
// Configuration commune pour les graphiques
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'dd/MM/yyyy'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
beginAtZero: true,
title: {
display: true
}
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
}
};
console.log('completionData', completionData)
const ctx = document.getElementById('themeChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
datasets: [
{
label: "Nombre d'objets",
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}
}
}
}
});
// Function to update the chart with new data
function updateChart() {
// Get the chart instance
const chartInstance = Chart.getChart('themeChart');
if (!chartInstance) return;
// Update the datasets
chartInstance.data.datasets[0].data = Array.isArray(window.countData)
? window.countData.map(d => ({x: new Date(d.date), y: d.value}))
: [];
if (Array.isArray(window.completionData)) {
chartInstance.data.datasets[1].data = window.completionData.map(d => ({
x: new Date(d.date),
y: d.value
}));
}
// Update the chart
chartInstance.update();
}
// Initialiser les statistiques
updateStats();
// Ajout debug JS requête Overpass
{% if overpass_query is defined %}
console.log('[DEBUG][Overpass] Requête envoyée à Overpass :', `{{ overpass_query|e('js') }}`);
{% else %}
console.log('[DEBUG][Overpass] Aucune requête Overpass transmise à la page.');
{% endif %}
// Fonction pour charger les analyses Osmose
function loadOsmoseAnalyses(map, theme, bbox) {
const items = osmoseItemsMapping[theme];
if (!items || items.length === 0) return;
const itemsParam = items.join(',');
const bboxParam = bbox.join(',');
const osmoseUrl = `https://osmose.openstreetmap.fr/api/0.3/issues?zoom=12&item=${itemsParam}&level=1,2,3&limit=500&bbox=${bboxParam}`;
fetch(osmoseUrl)
.then(response => response.json())
.then(data => {
if (!data.issues || data.issues.length === 0) {
console.log('Aucune analyse Osmose trouvée pour ce thème dans cette zone.');
return;
}
const divOsmose = document.querySelector(('#alertes_osmose'))
if(divOsmose){
divOsmose.innerHTML = `<span class="counter">${data.issues.length}</span> objets à ajouter selon Osmose`;
}
console.log(`[Osmose] ${data.issues.length} analyses trouvées pour le thème ${theme}`);
// Ajouter les marqueurs pour chaque analyse
data.issues.forEach(issue => {
if (issue.lat && issue.lon) {
let lapopup = new maplibregl.Popup({offset: 25})
.setHTML(
(() => {
return `<div id="osmose-popup-${issue.id}" >Proposition d'ajout <button onclick="loadOsmoseIssueDetails(${issue.id})">${issue.id}</button></div>`
})());
// lapopup.on('open', () => {
// // Charger les détails de l'analyse lorsque le popup est ouvert
// console.log('open popup', issue)
// // loadOsmoseIssueDetails(issue.id);
// });
// Créer un marqueur pour l'analyse
const marker = new maplibregl.Marker({
color: '#8A2BE2' // Violet
})
.setLngLat([issue.lon, issue.lat])
// Ajouter un popup au marqueur
.setPopup(
lapopup
)
.addTo(map);
console.log('marker', marker)
}
});
})
.catch(error => {
console.error('Erreur lors du chargement des analyses Osmose:', error);
});
}
// Fonction pour charger les détails d'une analyse Osmose
function loadOsmoseIssueDetails(issueId) {
const detailsUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}?langs=auto`;
console.log('loadOsmoseIssueDetails detailsUrl', detailsUrl)
fetch(detailsUrl)
.then(response => response.json())
.then(data => {
if (!data || !data.issue) return;
const issue = data.issue;
const popupElement = document.getElementById(`osmose-popup-${issueId}`);
if (!popupElement) return;
// Construire le contenu du popup
let popupContent = `
<h4>${issue.title || 'Analyse Osmose'}</h4>
<p>${issue.subtitle || ''}</p>
`;
// Ajouter les tags proposés s'ils existent
if (issue.fixes && issue.fixes.length > 0 && issue.fixes[0].tags) {
popupContent += '<div class="osmose-popup-tags">';
popupContent += '<h5>Tags proposés:</h5>';
Object.entries(issue.fixes[0].tags).forEach(([key, value]) => {
popupContent += `
<div class="osmose-popup-tag">
<span class="osmose-popup-tag-key">${key}</span>:
<span class="osmose-popup-tag-value">${value}</span>
</div>
`;
});
popupContent += '</div>';
}
// Ajouter les boutons d'action
popupContent += `
<div class="osmose-popup-buttons">
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Voir sur Osmose
</a>
<a href="http://localhost:8111/load_and_zoom?left=${issue.lon - 0.001}&right=${issue.lon + 0.001}&top=${issue.lat + 0.001}&bottom=${issue.lat - 0.001}" target="_blank" class="btn btn-sm btn-success">
<i class="bi bi-tools"></i> Réparer dans JOSM
</a>
</div>
`;
// Mettre à jour le contenu du popup
popupElement.innerHTML = popupContent;
})
.catch(error => {
console.error('Erreur lors du chargement des détails de l\'analyse Osmose:', error);
const popupElement = document.getElementById(`osmose-popup-${issueId}`);
if (popupElement) {
popupElement.innerHTML = '<div class="alert alert-danger">Erreur lors du chargement des détails.</div>';
}
});
}
</script>
{% endblock %}