osm-labo/templates/admin/followup_theme_graph.html.twig
2025-08-31 17:57:28 +02:00

1166 lines
52 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>
.osmose-infos .button{
color: #8A2BE2;
padding: 1rem;
}
#alertes_osmose .counter{
background: #8A2BE2;
border-radius: 10em;
margin-right: 1ch;
padding: 0.5rem;
color: white;
}
.bg-purple {
background-color: #8A2BE2 !important;
color: white !important;
}
#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 class="card mt-4 mb-4">
<div class="card-header">
<h4><i class="bi bi-exclamation-triangle"></i> Alertes Osmose</h4>
</div>
<div class="card-body">
<div id="alertes_osmose">Chargement des alertes...</div>
<div id="alertes_liste" class="mt-3" style="display: none;">
<h5>Liste complète des alertes</h5>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>ID</th>
<th>Élément</th>
<th>Position</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="alertes_table_body">
</tbody>
</table>
</div>
</div>
<div id="alertes_distribution" class="mt-4" style="display: none;">
<h5>Répartition des alertes par thème</h5>
<canvas id="alertesChart" height="200"></canvas>
</div>
</div>
</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],
'places': [7240, 8351, 8211, 7220, 8331, 8031]
// 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.');
document.querySelector('#alertes_osmose').innerHTML = '<div class="alert alert-info">Aucune alerte Osmose trouvée pour ce thème dans cette zone.</div>';
return;
}
// Stocker les données Osmose globalement pour pouvoir les utiliser ailleurs
window.osmoseData = data.issues;
// Mettre à jour le résumé des alertes
const divOsmose = document.querySelector('#alertes_osmose');
if(divOsmose){
if (data.issues.length === 1) {
// Si un seul objet, rendre tout le texte cliquable
const issueId = data.issues[0].id;
divOsmose.innerHTML = `<div class="alert alert-warning">
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" style="text-decoration: none; color: inherit;">
<span class="counter">${data.issues.length}</span> objet à ajouter selon Osmose
</a>
</div>`;
} else {
// Si plusieurs objets, afficher un résumé
divOsmose.innerHTML = `<div class="alert alert-warning">
<span class="counter">${data.issues.length}</span> objets à ajouter selon Osmose
</div>`;
}
}
// Remplir la table des alertes
const alertesTableBody = document.querySelector('#alertes_table_body');
if (alertesTableBody) {
let tableContent = '';
data.issues.forEach((issue, index) => {
const issueId = issue.id;
const element = issue.elems && issue.elems.length > 0 ?
`${issue.elems[0].type} ${issue.elems[0].id}` : 'Non spécifié';
const position = issue.lat && issue.lon ?
`${issue.lat.toFixed(5)}, ${issue.lon.toFixed(5)}` : 'Non spécifié';
tableContent += `
<tr>
<td>${issueId}</td>
<td>${element}</td>
<td>${position}</td>
<td>
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" class="btn btn-sm btn-info" title="Voir sur Osmose">
<i class="bi bi-eye"></i>
</a>
<a href="http://localhost:8111/import?url=https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}/fix/0" target="_blank" class="btn btn-sm btn-success" title="Corriger dans JOSM">
<i class="bi bi-tools"></i>
</a>
</td>
</tr>
`;
});
alertesTableBody.innerHTML = tableContent;
document.querySelector('#alertes_liste').style.display = 'block';
}
// Créer la distribution des alertes par thème
createAlertesDistribution(data.issues);
// Afficher la section de distribution
document.querySelector('#alertes_distribution').style.display = 'block';
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( `<div class="osmose-infos" id="osmose-popup-${issue.id}" > Intégration d'objet possible <a class="button" href="http://localhost:8111/import?url=https://osmose.openstreetmap.fr/api/0.3/issue/${issue.id}/fix/0">corriger dans josm</a> </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 créer le graphique de distribution des alertes par thème
function createAlertesDistribution(issues) {
if (!issues || issues.length === 0) return;
// Compter les alertes par item
const itemCounts = {};
const itemLabels = {
8410: 'Borne de recharge (manquante)',
8411: 'Borne de recharge (à compléter)',
8031: 'École (manquante)',
8211: 'Pharmacie (manquante)',
7220: 'Cabinet médical (manquant)',
8331: 'Hôpital (manquant)',
7240: 'Laboratoire (manquant)',
8351: 'Laboratoire (à compléter)',
8190: 'Police (manquante)',
8191: 'Police (à compléter)',
8370: 'Défibrillateur (manquant)'
};
issues.forEach(issue => {
if (issue.item) {
if (!itemCounts[issue.item]) {
itemCounts[issue.item] = 0;
}
itemCounts[issue.item]++;
}
});
// Préparer les données pour le graphique
const labels = [];
const data = [];
const backgroundColor = [];
// Générer des couleurs aléatoires pour chaque thème
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
// Trier les items par nombre d'alertes (décroissant)
const sortedItems = Object.keys(itemCounts).sort((a, b) => itemCounts[b] - itemCounts[a]);
sortedItems.forEach(item => {
// Utiliser le label s'il existe, sinon utiliser l'ID de l'item
labels.push(itemLabels[item] || `Item ${item}`);
data.push(itemCounts[item]);
backgroundColor.push(getRandomColor());
});
// Créer le graphique
const ctx = document.getElementById('alertesChart').getContext('2d');
new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: backgroundColor,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right',
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
});
}
// 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 %}