documentation wiki osm, ajout dashboard issues osmose
This commit is contained in:
parent
b28f8eac63
commit
7665f1d99c
12 changed files with 1758 additions and 76 deletions
196
templates/admin/completion_statistics.html.twig
Normal file
196
templates/admin/completion_statistics.html.twig
Normal file
|
@ -0,0 +1,196 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Statistiques de complétion{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.data-table {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.level-selector .btn {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.theme-selector {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<h1>Statistiques de complétion</h1>
|
||||
|
||||
<div class="filters">
|
||||
<form method="get" action="{{ path('app_admin_completion_statistics') }}" class="row">
|
||||
<div class="col-md-12">
|
||||
<h5>Niveau géographique</h5>
|
||||
<div class="level-selector">
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'department', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'department' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
Par département
|
||||
</a>
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'region', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'region' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
Par région
|
||||
</a>
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'country', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'country' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
France entière
|
||||
</a>
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'city', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'city' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
Par ville
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 theme-selector">
|
||||
<h5>Thématique</h5>
|
||||
<select name="theme" class="form-select" onchange="this.form.submit()">
|
||||
{% for themeKey, themeLabel in themes %}
|
||||
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
||||
{{ themeLabel }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="completionChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="data-table">
|
||||
<h3>Données détaillées</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if level == 'department' %}
|
||||
<th>Code département</th>
|
||||
{% elseif level == 'region' %}
|
||||
<th>Région</th>
|
||||
{% elseif level == 'country' %}
|
||||
<th>Pays</th>
|
||||
{% else %}
|
||||
<th>Ville</th>
|
||||
{% endif %}
|
||||
<th>Taux de complétion (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, value in completionData %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctx = document.getElementById('completionChart').getContext('2d');
|
||||
|
||||
// Définir les couleurs en fonction du niveau géographique
|
||||
let backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
||||
let borderColor = 'rgba(54, 162, 235, 1)';
|
||||
|
||||
{% if level == 'department' %}
|
||||
backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
||||
borderColor = 'rgba(54, 162, 235, 1)';
|
||||
{% elseif level == 'region' %}
|
||||
backgroundColor = 'rgba(255, 159, 64, 0.5)';
|
||||
borderColor = 'rgba(255, 159, 64, 1)';
|
||||
{% elseif level == 'country' %}
|
||||
backgroundColor = 'rgba(75, 192, 192, 0.5)';
|
||||
borderColor = 'rgba(75, 192, 192, 1)';
|
||||
{% else %}
|
||||
backgroundColor = 'rgba(153, 102, 255, 0.5)';
|
||||
borderColor = 'rgba(153, 102, 255, 1)';
|
||||
{% endif %}
|
||||
|
||||
// Créer le graphique
|
||||
const completionChart = new Chart(ctx, {
|
||||
type: {% if level == 'country' %}'bar'{% else %}'bar'{% endif %},
|
||||
data: {
|
||||
labels: {{ chartLabels|raw }},
|
||||
datasets: [{
|
||||
label: 'Taux de complétion (%)',
|
||||
data: {{ chartData|raw }},
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Taux de complétion (%)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: {% if level == 'department' %}
|
||||
'Départements'
|
||||
{% elseif level == 'region' %}
|
||||
'Régions'
|
||||
{% elseif level == 'country' %}
|
||||
'Pays'
|
||||
{% else %}
|
||||
'Villes'
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Taux de complétion par {{ level == "department" ? "département" : (level == "region" ? "région" : (level == "country" ? "pays" : "ville")) }} pour {{ themes[theme] }}',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': ' + context.raw + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
353
templates/admin/osmose_issues_map.html.twig
Normal file
353
templates/admin/osmose_issues_map.html.twig
Normal file
|
@ -0,0 +1,353 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Carte des problèmes Osmose - {{ city.name }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
#map {
|
||||
height: 70vh;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.issue-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.issue-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.issue-item.level-1 {
|
||||
border-left-color: #dc3545; /* Rouge pour les erreurs critiques */
|
||||
}
|
||||
.issue-item.level-2 {
|
||||
border-left-color: #fd7e14; /* Orange pour les erreurs importantes */
|
||||
}
|
||||
.issue-item.level-3 {
|
||||
border-left-color: #ffc107; /* Jaune pour les avertissements */
|
||||
}
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
}
|
||||
.marker-level-1 {
|
||||
background-color: #dc3545;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.marker-level-2 {
|
||||
background-color: #fd7e14;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.marker-level-3 {
|
||||
background-color: #ffc107;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.no-issues {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<h1>Problèmes Osmose pour {{ city.name }}</h1>
|
||||
|
||||
<div class="filters">
|
||||
<form method="get" action="{{ path('app_admin_osmose_issues_map', {'inseeCode': city.zone}) }}" class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="theme">Filtrer par thème</label>
|
||||
<select name="theme" id="theme" class="form-select" onchange="this.form.submit()">
|
||||
<option value="all" {{ theme == 'all' ? 'selected' : '' }}>Tous les thèmes</option>
|
||||
{% for themeKey, themeLabel in themes %}
|
||||
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
||||
{{ themeLabel }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Liste des problèmes ({{ osmoseIssues|length }})</h3>
|
||||
|
||||
{% if osmoseIssues|length > 0 %}
|
||||
<div class="issue-list">
|
||||
{% for issue in osmoseIssues %}
|
||||
<div class="issue-item level-{{ issue.level }}" data-lat="{{ issue.lat }}" data-lon="{{ issue.lon }}">
|
||||
<h5>{{ issue.title }}</h5>
|
||||
{% if issue.subtitle %}
|
||||
<p>{{ issue.subtitle }}</p>
|
||||
{% endif %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="badge bg-secondary">Item: {{ issue.item }}</span>
|
||||
<a href="{{ issue.url }}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-issues">
|
||||
<p>Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialiser la carte avec MapLibre
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: 'https://demotiles.maplibre.org/style.json', // style URL
|
||||
center: [{{ city.lon }}, {{ city.lat }}], // Note: MapLibre uses [longitude, latitude] order
|
||||
zoom: 13
|
||||
});
|
||||
|
||||
// Ajouter les contrôles de navigation
|
||||
map.addControl(new maplibregl.NavigationControl());
|
||||
|
||||
// Attendre que la carte soit chargée
|
||||
map.on('load', function() {
|
||||
// Créer une source de données pour les marqueurs
|
||||
const features = [];
|
||||
|
||||
// Ajouter les marqueurs pour chaque problème
|
||||
{% for issue in osmoseIssues %}
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [{{ issue.lon }}, {{ issue.lat }}]
|
||||
},
|
||||
properties: {
|
||||
title: "{{ issue.title|e('js') }}",
|
||||
subtitle: "{{ issue.subtitle|e('js') }}",
|
||||
item: "{{ issue.item }}",
|
||||
url: "{{ issue.url }}",
|
||||
level: "{{ issue.level }}"
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Ajouter la source de données à la carte
|
||||
map.addSource('issues', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: features
|
||||
},
|
||||
cluster: true,
|
||||
clusterMaxZoom: 14,
|
||||
clusterRadius: 50
|
||||
});
|
||||
|
||||
// Ajouter une couche pour les clusters
|
||||
map.addLayer({
|
||||
id: 'clusters',
|
||||
type: 'circle',
|
||||
source: 'issues',
|
||||
filter: ['has', 'point_count'],
|
||||
paint: {
|
||||
'circle-color': [
|
||||
'step',
|
||||
['get', 'point_count'],
|
||||
'rgba(181, 226, 140, 0.6)',
|
||||
10,
|
||||
'rgba(241, 211, 87, 0.6)',
|
||||
30,
|
||||
'rgba(253, 156, 115, 0.6)'
|
||||
],
|
||||
'circle-radius': [
|
||||
'step',
|
||||
['get', 'point_count'],
|
||||
20,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
40
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter une couche pour le nombre de points dans chaque cluster
|
||||
map.addLayer({
|
||||
id: 'cluster-count',
|
||||
type: 'symbol',
|
||||
source: 'issues',
|
||||
filter: ['has', 'point_count'],
|
||||
layout: {
|
||||
'text-field': '{point_count_abbreviated}',
|
||||
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
|
||||
'text-size': 12
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter une couche pour les points individuels
|
||||
map.addLayer({
|
||||
id: 'unclustered-point',
|
||||
type: 'circle',
|
||||
source: 'issues',
|
||||
filter: ['!', ['has', 'point_count']],
|
||||
paint: {
|
||||
'circle-color': [
|
||||
'match',
|
||||
['get', 'level'],
|
||||
'1', '#dc3545',
|
||||
'2', '#fd7e14',
|
||||
'3', '#ffc107',
|
||||
'#007bff'
|
||||
],
|
||||
'circle-radius': 10,
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#fff'
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter un événement de clic sur les clusters
|
||||
map.on('click', 'clusters', function(e) {
|
||||
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
|
||||
const clusterId = features[0].properties.cluster_id;
|
||||
|
||||
map.getSource('issues').getClusterExpansionZoom(clusterId, function(err, zoom) {
|
||||
if (err) return;
|
||||
|
||||
map.easeTo({
|
||||
center: features[0].geometry.coordinates,
|
||||
zoom: zoom
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Ajouter un événement de clic sur les points individuels
|
||||
map.on('click', 'unclustered-point', function(e) {
|
||||
const coordinates = e.features[0].geometry.coordinates.slice();
|
||||
const title = e.features[0].properties.title;
|
||||
const subtitle = e.features[0].properties.subtitle;
|
||||
const item = e.features[0].properties.item;
|
||||
const url = e.features[0].properties.url;
|
||||
|
||||
// Créer le contenu de la popup
|
||||
let popupContent = `
|
||||
<h5>${title}</h5>
|
||||
`;
|
||||
|
||||
if (subtitle) {
|
||||
popupContent += `<p>${subtitle}</p>`;
|
||||
}
|
||||
|
||||
popupContent += `
|
||||
<div>
|
||||
<span class="badge bg-secondary">Item: ${item}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href="${url}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assurer que si le zoom change, la popup reste à la bonne position
|
||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
||||
}
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(popupContent)
|
||||
.addTo(map);
|
||||
});
|
||||
|
||||
// Changer le curseur au survol des clusters et des points
|
||||
map.on('mouseenter', 'clusters', function() {
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
map.on('mouseleave', 'clusters', function() {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
map.on('mouseenter', 'unclustered-point', function() {
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
map.on('mouseleave', 'unclustered-point', function() {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
|
||||
// Ajouter un événement de clic sur les éléments de la liste
|
||||
{% for issue in osmoseIssues %}
|
||||
document.querySelector(`.issue-item[data-lat="{{ issue.lat }}"][data-lon="{{ issue.lon }}"]`)?.addEventListener('click', function() {
|
||||
map.flyTo({
|
||||
center: [{{ issue.lon }}, {{ issue.lat }}],
|
||||
zoom: 18
|
||||
});
|
||||
|
||||
// Simuler un clic sur le point pour ouvrir la popup
|
||||
const features = map.queryRenderedFeatures(
|
||||
map.project([{{ issue.lon }}, {{ issue.lat }}]),
|
||||
{ layers: ['unclustered-point'] }
|
||||
);
|
||||
|
||||
if (features.length > 0) {
|
||||
map.fire('click', {
|
||||
lngLat: { lng: {{ issue.lon }}, lat: {{ issue.lat }} },
|
||||
point: map.project([{{ issue.lon }}, {{ issue.lat }}]),
|
||||
features: [features[0]]
|
||||
});
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Ajuster la vue pour montrer tous les marqueurs si nécessaire
|
||||
if (features.length > 0) {
|
||||
// Calculer les limites de tous les points
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
features.forEach(function(feature) {
|
||||
bounds.extend(feature.geometry.coordinates);
|
||||
});
|
||||
|
||||
map.fitBounds(bounds, {
|
||||
padding: 50
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -47,6 +47,7 @@
|
|||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ path('app_admin_podium_contributeurs_osm') }}"><i class="bi bi-trophy-fill"></i> Podium des contributeurs OSM</a></li>
|
||||
<li><a class="dropdown-item" href="{{ path('admin_followup_global_graph') }}"><i class="bi bi-globe"></i> Suivi global OSM</a></li>
|
||||
<li><a class="dropdown-item" href="{{ path('app_admin_osmose_issues_map') }}"><i class="bi bi-exclamation-triangle-fill"></i> Problèmes Osmose</a></li>
|
||||
<li><a class="dropdown-item" href="{{ path('app_admin_import_stats') }}"><i class="bi bi-upload"></i> Import Stats</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue