988 lines
44 KiB
Twig
988 lines
44 KiB
Twig
{% 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 %}
|