osm-commerces/templates/admin/followup_theme_graph.html.twig

497 lines
No EOL
21 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'/>
<style>
.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;
}
.btn-josm {
margin-bottom: 1rem;
}
</style>
{% endblock %}
{% block body %}
<div class="container-fluid">
{# 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>
<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>
<a href="{{ path('app_public_stats_evolutions', {'insee_code': stats.zone}) }}" class="btn btn-outline-primary">
<i class="bi bi-clock-history"></i> Évolutions temporelles
</a>
<a href="{{ path('admin_street_completion', {'insee_code': stats.zone}) }}" class="btn btn-outline-success">
<i class="bi bi-signpost"></i> Complétion des rues
</a>
</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 %}
<div id="themeMap"></div>
{% 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 %}
<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>
<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>
</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><code>{{ tag }}</code></li>
{% else %}
<li><span class="text-muted">Aucun critère défini</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<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...');
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
}
]
};
}
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>
<a href='${f.properties.osm_url}' target='_blank'>Voir sur OSM</a><br>
${f.properties.uuid ? `<a href='/edit/${f.properties.zip_code}/${encodeURIComponent(f.properties.name)}/${f.properties.uuid}' target='_blank'>📝 Modifier ce lieu</a>` : ''}
</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);
});
}
} 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>
<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
}
};
// Graphique fusionné
const ctx = document.getElementById('themeChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
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',
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: true },
tooltip: { mode: 'index', intersect: false }
},
interaction: { mode: 'nearest', axis: 'x', intersect: false },
scales: {
x: {
type: 'time',
time: { unit: 'day', displayFormats: { day: 'dd/MM/yyyy' } },
title: { display: true, text: 'Date' }
},
y1: {
type: 'linear',
position: 'left',
title: { display: true, text: "Nombre d'objets" },
beginAtZero: true
},
y2: {
type: 'linear',
position: 'right',
title: { display: true, text: 'Complétion (%)' },
min: 0,
max: 100,
grid: { drawOnChartArea: false }
}
}
}
});
// Initialiser les statistiques
updateStats();
// 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 %}
</script>
{% endblock %}