2025-07-05 12:37:01 +02:00
{% extends 'base_embed.html.twig' %}
{% block title %} Graphique {{ theme_label }} - {{ stats .name }} {% endblock %}
{% block stylesheets %}
{{ parent ( ) }}
2025-07-05 15:25:33 +02:00
<link href=' {{ asset ( 'js/maplibre/maplibre-gl.css' ) }} ' rel='stylesheet'/>
2025-07-05 12:37:01 +02:00
<style>
.chart-container {
width: 100%;
height: 400px;
margin: 20px 0;
}
.stats-header {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
2025-07-05 13:00:00 +02:00
.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;
}
2025-07-05 12:37:01 +02:00
.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;
}
2025-07-05 15:25:33 +02:00
#themeMap {
height: 400px;
width: 100%;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.maplibregl-popup-content {
font-size: 0.95em;
}
.btn-josm {
margin-bottom: 1rem;
}
2025-07-05 12:37:01 +02:00
</style>
{% endblock %}
{% block body %}
<div class="container-fluid">
2025-07-05 15:25:33 +02:00
{# 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 %}
2025-07-05 12:37:01 +02:00
<div class="stats-header">
2025-07-05 13:00:00 +02:00
<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>
<div class="action-bar">
<a href=" {{ path ( 'app_admin_stats' , { 'insee_code' : stats .zone } ) }} " class="btn btn-primary">
<i class="bi bi-bar-chart"></i> Stats de la ville
</a>
<a href=" {{ path ( 'app_admin_labourer' , { 'insee_code' : stats .zone } ) }} " class="btn btn-warning">
<i class="bi bi-arrow-clockwise"></i> Labourer la ville
</a>
<a href=" {{ path ( 'admin_followup_graph' , { 'insee_code' : stats .zone } ) }} " class="btn btn-info">
<i class="bi bi-graph-up"></i> Suivi OSM (graphes)
</a>
<a href=" {{ path ( 'admin_followup_embed_graph' , { 'insee_code' : stats .zone , 'theme' : theme } ) }} " class="btn btn-success" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Graphe embedded
</a>
<a href=" {{ path ( 'app_admin' ) }} " class="btn btn-secondary">
<i class="bi bi-house"></i> Accueil admin
</a>
2025-07-05 12:37:01 +02:00
</div>
2025-07-05 15:25:33 +02:00
{% 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 %}
<div id="themeMap"></div>
2025-07-05 16:15:56 +02:00
{% if overpass_query is defined %}
<div class="mb-3">
<a href="https://overpass-turbo.eu/?Q= {{ overpass_query | url_encode }} " target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Voir la requête sur Overpass Turbo
</a>
</div>
{% endif %}
2025-07-05 12:37:01 +02:00
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="currentCount">-</div>
<div class="stat-label">Nombre actuel</div>
</div>
<div class="stat-card">
<div class="stat-value" id="currentCompletion">-</div>
<div class="stat-label">Complétion actuelle</div>
</div>
<div class="stat-card">
<div class="stat-value" id="dataPoints">-</div>
<div class="stat-label">Points de données</div>
</div>
<div class="stat-card">
<div class="stat-value" id="lastUpdate">-</div>
<div class="stat-label">Dernière mise à jour</div>
</div>
</div>
2025-07-05 16:15:56 +02:00
<div class="row mb-3">
<div class="col-md-6">
{ # <div class="card p-3">
<h5>Progression</h5>
<table class="table table-sm mb-0">
<thead><tr><th>Période</th><th>Nombre</th><th>Complétion (%)</th></tr></thead>
<tbody>
<tr><td>7 jours</td><td> {{ progressions .count [ '7j' ] is not null ? '%+d' | format ( progressions .count [ '7j' ] ) : '– ' }} </td><td> {{ progressions .completion [ '7j' ] is not null ? '%+.1f' | format ( progressions .completion [ '7j' ] ) : '– ' }} </td></tr>
<tr><td>30 jours</td><td> {{ progressions .count [ '30j' ] is not null ? '%+d' | format ( progressions .count [ '30j' ] ) : '– ' }} </td><td> {{ progressions .completion [ '30j' ] is not null ? '%+.1f' | format ( progressions .completion [ '30j' ] ) : '– ' }} </td></tr>
<tr><td>6 mois</td><td> {{ progressions .count [ '6m' ] is not null ? '%+d' | format ( progressions .count [ '6m' ] ) : '– ' }} </td><td> {{ progressions .completion [ '6m' ] is not null ? '%+.1f' | format ( progressions .completion [ '6m' ] ) : '– ' }} </td></tr>
<tr><td>1 an</td><td> {{ progressions .count [ '1a' ] is not null ? '%+d' | format ( progressions .count [ '1a' ] ) : '– ' }} </td><td> {{ progressions .completion [ '1a' ] is not null ? '%+.1f' | format ( progressions .completion [ '1a' ] ) : '– ' }} </td></tr>
</tbody>
</table>
</div> #}
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="ms-auto">
<label for="basemapSelect" class="form-label mb-1">Fond de carte :</label>
<select id="basemapSelect" class="form-select">
<option value="streets">MapTiler Streets</option>
<option value="satellite">BD Ortho IGN</option>
</select>
</div>
2025-07-05 12:37:01 +02:00
</div>
</div>
2025-07-05 16:15:56 +02:00
<div class="chart-container">
<canvas id="themeChart"></canvas>
2025-07-05 12:37:01 +02:00
</div>
2025-07-05 17:21:18 +02:00
{% 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><code> {{ tag }} </code></li>
{% else %}
<li><span class="text-muted">Aucun critère défini</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
2025-07-05 12:37:01 +02:00
</div>
{% endblock %}
{% block javascripts %}
{{ parent ( ) }}
2025-07-05 15:25:33 +02:00
<script src=' {{ asset ( 'js/maplibre/maplibre-gl.js' ) }} '></script>
<script>
const geojson = {{ geojson | raw }} ;
const mapToken = ' {{ maptiler_token }} ';
const mapCenter = {{ center | json_encode | raw }} ;
document.addEventListener('DOMContentLoaded', function() {
console.log('[DEBUG] mapToken:', mapToken);
console.log('[DEBUG] geojson:', geojson);
console.log('[DEBUG] geojson.features:', geojson ? geojson.features : undefined);
console.log('[DEBUG] mapCenter:', mapCenter);
if (mapToken && geojson && geojson.features && geojson.features.length > 0) {
console.log('[DEBUG] Initialisation de la carte Maplibre...');
2025-07-05 16:15:56 +02:00
let mapInstance = null;
function getStyleUrl(style) {
if (style === 'streets') {
return `https://api.maptiler.com/maps/streets/style.json?key=$ { mapToken}`;
} else if (style === 'satellite') {
// BD Ortho IGN WMTS (clé publique, usage limité)
return {
version: 8,
sources: {
"bdortho": {
"type": "raster",
"tiles": [
"https://wxs.ign.fr/essentiels/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}"
],
"tileSize": 256,
"attribution": "Données © IGN BD Ortho"
}
},
layers: [
{
"id": "bdortho",
"type": "raster",
"source": "bdortho",
"minzoom": 0,
"maxzoom": 19
}
]
};
2025-07-05 15:25:33 +02:00
}
2025-07-05 16:15:56 +02:00
return null;
}
function initMap(style) {
if (mapInstance) { mapInstance.remove(); }
const styleUrl = getStyleUrl(style);
mapInstance = new maplibregl.Map( {
container: 'themeMap',
style: styleUrl,
center: mapCenter || (geojson.features && geojson.features[0] ? geojson.features[0].geometry.coordinates : [2,48]),
zoom: 13
});
mapInstance.addControl(new maplibregl.NavigationControl());
geojson.features.forEach(f => {
let color = f.properties.is_complete ? '#198754' : '#adb5bd';
if (!f.properties.is_complete && (f.properties.tags && (f.properties.tags.name || f.properties.tags.operator))) {
color = '#ffc107';
}
const marker = new maplibregl.Marker( { color: color })
.setLngLat(f.geometry.coordinates)
.setPopup(new maplibregl.Popup( { offset: 18 })
.setHTML(`
<div style='min-width:180px'>
<strong>$ { f.properties.name || '(sans nom)'}</strong><br>
<span class='text-muted'>$ { f.properties.osm_kind} $ { f.properties.id}</span><br>
<span style='font-size:0.95em;'>
$ { Object.entries(f.properties.tags).map(([k,v]) => `<span><b>$ { k}</b>: $ { v}</span>`).join('<br>')}
</span>
2025-07-05 16:34:37 +02:00
<a href='$ { f.properties.osm_url}' target='_blank'>Voir sur OSM</a><br>
2025-07-05 16:53:12 +02:00
$ { f.properties.uuid ? `<a href='/edit/$ { f.properties.zip_code}/$ { encodeURIComponent(f.properties.name)}/$ { f.properties.uuid}' target='_blank'>📝 Modifier ce lieu</a>` : ''}
2025-07-05 16:15:56 +02:00
</div>
`)
)
.addTo(mapInstance);
});
}
// Initialisation par défaut
initMap('streets');
// Sélecteur de fond de carte
const basemapSelect = document.getElementById('basemapSelect');
if (basemapSelect) {
basemapSelect.addEventListener('change', function() {
initMap(this.value);
});
}
2025-07-05 15:25:33 +02:00
} else {
console.warn('[DEBUG] Carte non initialisée : conditions non remplies.');
if (!mapToken) {
console.warn('[DEBUG] mapToken manquant ou vide');
}
if (!geojson) {
console.warn('[DEBUG] geojson manquant ou vide');
}
if (!geojson || !geojson.features || geojson.features.length === 0) {
console.warn('[DEBUG] geojson.features vide ou non défini');
}
// Affichage d'un message dans la page
const mapDiv = document.getElementById('themeMap');
if (mapDiv) {
mapDiv.innerHTML = '<div style="color:red;font-weight:bold;padding:2em;text-align:center;">Carte non initialisée : données manquantes ou incomplètes (voir console pour debug)</div>';
}
}
});
</script>
2025-07-05 12:37:01 +02:00
<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 | raw }} ;
const completionData = {{ completion_data | raw }} ;
// Mettre à jour les statistiques
function updateStats() {
if (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 (completionData.length > 0) {
const latestCompletion = completionData[completionData.length - 1];
document.getElementById('currentCompletion').textContent = latestCompletion.value + '%';
}
document.getElementById('dataPoints').textContent = Math.max(countData.length, completionData.length);
}
// 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
}
};
2025-07-05 16:15:56 +02:00
// Graphique fusionné
const ctx = document.getElementById('themeChart').getContext('2d');
new Chart(ctx, {
2025-07-05 12:37:01 +02:00
type: 'line',
data: {
2025-07-05 16:15:56 +02:00
datasets: [
{
label: "Nombre d'objets",
data: 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: 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',
2025-07-05 12:37:01 +02:00
}
2025-07-05 16:15:56 +02:00
]
2025-07-05 12:37:01 +02:00
},
options: {
2025-07-05 16:15:56 +02:00
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: true },
tooltip: { mode: 'index', intersect: false }
},
interaction: { mode: 'nearest', axis: 'x', intersect: false },
2025-07-05 12:37:01 +02:00
scales: {
2025-07-05 16:15:56 +02:00
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,
2025-07-05 12:37:01 +02:00
max: 100,
2025-07-05 16:15:56 +02:00
grid: { drawOnChartArea: false }
2025-07-05 12:37:01 +02:00
}
}
}
});
// Initialiser les statistiques
updateStats();
2025-07-05 16:15:56 +02:00
// 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 %}
2025-07-05 12:37:01 +02:00
</script>
{% endblock %}