suivi global
This commit is contained in:
parent
afc120ef2a
commit
e3f8680472
6 changed files with 531 additions and 48 deletions
105
templates/admin/followup_global_graph.html.twig
Normal file
105
templates/admin/followup_global_graph.html.twig
Normal file
|
@ -0,0 +1,105 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
{% block title %}Suivi global de toutes les villes{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<h1>Suivi global de toutes les villes</h1>
|
||||
<div class="mb-4">
|
||||
<h3>Nombre de villes et complétion moyenne</h3>
|
||||
<canvas id="global-summary-chart" height="80"></canvas>
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Suivi par thématique</h3>
|
||||
<div class="row">
|
||||
{% set themes = [
|
||||
'fire_hydrant', 'charging_station', 'toilets', 'bus_stop', 'defibrillator', 'camera', 'recycling', 'substation', 'laboratory', 'school', 'police', 'healthcare', 'places'
|
||||
] %}
|
||||
{% for theme in themes %}
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ theme|replace({'_': ' '})|title }}</h5>
|
||||
<canvas id="chart-{{ theme }}-count" height="60"></canvas>
|
||||
<canvas id="chart-{{ theme }}-completion" height="60"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
|
||||
<script>
|
||||
const series = {{ series|json_encode|raw }};
|
||||
// Graphe global summary
|
||||
const ctxSummary = document.getElementById('global-summary-chart').getContext('2d');
|
||||
const cityCountData = (series['city_count'] || []).map(e => ({x: e.date, y: e.value}));
|
||||
const completionAvgData = (series['global_completion_average'] || []).map(e => ({x: e.date, y: e.value}));
|
||||
new Chart(ctxSummary, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nombre de villes',
|
||||
data: cityCountData,
|
||||
borderColor: 'blue',
|
||||
yAxisID: 'y1',
|
||||
},
|
||||
{
|
||||
label: 'Complétion moyenne (%)',
|
||||
data: completionAvgData,
|
||||
borderColor: 'green',
|
||||
yAxisID: 'y2',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
interaction: {mode: 'index', intersect: false},
|
||||
stacked: false,
|
||||
scales: {
|
||||
x: {type: 'time', time: {unit: 'day'}},
|
||||
y1: {type: 'linear', position: 'left', title: {display: true, text: 'Villes'}},
|
||||
y2: {type: 'linear', position: 'right', title: {display: true, text: '%'}},
|
||||
}
|
||||
}
|
||||
});
|
||||
// Graphes par thème
|
||||
const themes = {{ themes|json_encode|raw }};
|
||||
themes.forEach(theme => {
|
||||
// Count
|
||||
const countData = (series[theme + '_count'] || []).map(e => ({x: e.date, y: e.value}));
|
||||
const ctxCount = document.getElementById('chart-' + theme + '-count').getContext('2d');
|
||||
new Chart(ctxCount, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Nombre',
|
||||
data: countData,
|
||||
borderColor: 'orange',
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {x: {type: 'time', time: {unit: 'day'}}, y: {beginAtZero: true}}
|
||||
}
|
||||
});
|
||||
// Completion
|
||||
const completionData = (series[theme + '_completion'] || []).map(e => ({x: e.date, y: e.value}));
|
||||
const ctxCompletion = document.getElementById('chart-' + theme + '-completion').getContext('2d');
|
||||
new Chart(ctxCompletion, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Complétion (%)',
|
||||
data: completionData,
|
||||
borderColor: 'green',
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {x: {type: 'time', time: {unit: 'day'}}, y: {beginAtZero: true, max: 100}}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -5,10 +5,16 @@
|
|||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<h1>Suivi des objets OSM pour {{ stats.name }} ({{ stats.zone }})</h1>
|
||||
<div class="mb-3">
|
||||
<div class="mb-3 d-flex flex-wrap gap-2">
|
||||
<a href="{{ path('admin_followup', {'insee_code': stats.zone}) }}" class="btn btn-warning">
|
||||
<i class="bi bi-arrow-repeat"></i> Mettre à jour les suivis (followup)
|
||||
</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1}) }}" class="btn btn-primary">
|
||||
<i class="bi bi-shovel"></i> Labourer la zone
|
||||
</a>
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-info">
|
||||
<i class="bi bi-bar-chart"></i> Voir les stats
|
||||
</a>
|
||||
</div>
|
||||
<p>Historique des objets suivis (nombre et complétion).</p>
|
||||
{% set type_labels = {
|
||||
|
@ -21,7 +27,9 @@
|
|||
'recycling': 'Points de recyclage',
|
||||
'substation': 'Sous-stations électriques',
|
||||
'laboratory': "Laboratoires d'analyse",
|
||||
'school': 'Écoles'
|
||||
'school': 'Écoles',
|
||||
'police': 'Commissariats',
|
||||
'healthcare': 'Lieux de santé'
|
||||
} %}
|
||||
{% for type in type_labels|keys %}
|
||||
<h2 id="title-{{ type }}">{{ type_labels[type] }}</h2>
|
||||
|
@ -91,7 +99,9 @@
|
|||
recycling: 'Points de recyclage',
|
||||
substation: 'Sous-stations électriques',
|
||||
laboratory: "Laboratoires d'analyse",
|
||||
school: 'Écoles'
|
||||
school: 'Écoles',
|
||||
police: 'Commissariats',
|
||||
healthcare: 'Lieux de santé'
|
||||
};
|
||||
function formatDelta(val) {
|
||||
if (val === null) return '-';
|
||||
|
|
|
@ -41,6 +41,27 @@
|
|||
border-left: 4px solid #0dcaf0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.completion-badge {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 4px;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 2px #888;
|
||||
}
|
||||
.completion-low {
|
||||
background: #b2dfdb;
|
||||
border-color: #009688;
|
||||
}
|
||||
.completion-medium {
|
||||
background: #81c784;
|
||||
border-color: #388e3c;
|
||||
}
|
||||
.completion-high {
|
||||
background: #388e3c;
|
||||
border-color: #1b5e20;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -107,6 +128,98 @@
|
|||
|
||||
|
||||
<div class="row">
|
||||
<div id="followups">
|
||||
|
||||
|
||||
{% set followup_icons = {
|
||||
'fire_hydrant': 'bi-droplet',
|
||||
'charging_station': 'bi-lightning-charge',
|
||||
'toilets': 'bi-toilet',
|
||||
'bus_stop': 'bi-bus-front',
|
||||
'defibrillator': 'bi-heart-pulse',
|
||||
'camera': 'bi-camera-video',
|
||||
'recycling': 'bi-recycle',
|
||||
'substation': 'bi-plug',
|
||||
'laboratory': 'bi-beaker',
|
||||
'school': 'bi-mortarboard',
|
||||
'police': 'bi-shield-lock',
|
||||
'healthcare': 'bi-hospital',
|
||||
'places': 'bi-geo-alt'
|
||||
} %}
|
||||
{% set type_labels = {
|
||||
'fire_hydrant': 'Bornes incendie',
|
||||
'charging_station': 'Bornes de recharge',
|
||||
'toilets': 'Toilettes publiques',
|
||||
'bus_stop': 'Arrêts de bus',
|
||||
'defibrillator': 'Défibrillateurs',
|
||||
'camera': 'Caméras de surveillance',
|
||||
'recycling': 'Points de recyclage',
|
||||
'substation': 'Sous-stations électriques',
|
||||
'laboratory': "Laboratoires d'analyse",
|
||||
'school': 'Écoles',
|
||||
'police': 'Commissariats',
|
||||
'healthcare': 'Lieux de santé',
|
||||
'places': 'Lieux'
|
||||
} %}
|
||||
|
||||
{% set overpass_type_queries = {
|
||||
'fire_hydrant': 'nwr["emergency"="fire_hydrant"](area.searchArea);',
|
||||
'charging_station': 'nwr["amenity"="charging_station"](area.searchArea);',
|
||||
'toilets': 'nwr["amenity"="toilets"](area.searchArea);',
|
||||
'bus_stop': 'nwr["highway"="bus_stop"](area.searchArea);',
|
||||
'defibrillator': 'nwr["emergency"="defibrillator"](area.searchArea);',
|
||||
'camera': 'nwr["man_made"="surveillance"](area.searchArea);',
|
||||
'recycling': 'nwr["amenity"="recycling"](area.searchArea);',
|
||||
'substation': 'nwr["power"="substation"](area.searchArea);',
|
||||
'laboratory': 'nwr["healthcare"="laboratory"](area.searchArea);',
|
||||
'school': 'nwr["amenity"="school"](area.searchArea);',
|
||||
'police': 'nwr["amenity"="police"](area.searchArea);',
|
||||
'healthcare': 'nwr["healthcare"](area.searchArea);nwr["amenity"="doctors"](area.searchArea);nwr["amenity"="pharmacy"](area.searchArea);nwr["amenity"="hospital"](area.searchArea);nwr["amenity"="clinic"](area.searchArea);nwr["amenity"="social_facility"](area.searchArea);'
|
||||
} %}
|
||||
|
||||
<div class="row mb-4 latestFollowups ">
|
||||
{% for type, data in latestFollowups %}
|
||||
{% set overpass_query = '[out:json][timeout:60];\narea["ref:INSEE"="' ~ stats.zone ~ '"]->.searchArea;\n(' ~ overpass_type_queries[type]|default('') ~ ');\n(._;>;);\nout meta;\n>;' %}
|
||||
{% set completion = data.completion is defined ? data.completion.getMeasure() : null %}
|
||||
{% set completion_class = '' %}
|
||||
{% if completion is not null %}
|
||||
{% if completion < 40 %}
|
||||
{% set completion_class = 'completion-low' %}
|
||||
{% elseif completion < 80 %}
|
||||
{% set completion_class = 'completion-medium' %}
|
||||
{% else %}
|
||||
{% set completion_class = 'completion-high' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data is defined and (data.count is defined or data.completion is defined) %}
|
||||
<div class="col-auto mb-2">
|
||||
<div class="card shadow-sm text-center" style="min-width: 140px;">
|
||||
<div class="card-body p-2">
|
||||
<span class="completion-badge {{ completion_class }}"></span><br>
|
||||
<i class="bi {{ followup_icons[type]|default('bi-question-circle') }} fs-2 mb-1"></i><br>
|
||||
<a href="http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode }}" target="_blank" class="fw-bold text-decoration-underline text-dark" title="Charger dans JOSM">{{ type_labels[type]|default(type|capitalize) }}</a><br>
|
||||
<span title="Nombre"> {{ data.count is defined ? data.count.getMeasure() : '?' }}</span><br>
|
||||
<span title="Complétion"> {{ completion is not null ? completion : '?' }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-auto mb-2">
|
||||
<div class="card shadow-sm text-center bg-light text-muted" style="min-width: 140px;">
|
||||
<div class="card-body p-2">
|
||||
<span class="completion-badge" style="background:#eee;"></span><br>
|
||||
<i class="bi bi-question-circle fs-2 mb-1"></i><br>
|
||||
<span class="fw-bold">{{ type_labels[type]|default(type|capitalize) }}</span><br>
|
||||
<span title="Nombre">N = ?</span><br>
|
||||
<span title="Complétion">?%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<span class="badge {% if stats.getCompletionPercent() > 85 %}bg-success{% else %}bg-warning{% endif %}">
|
||||
{{ stats.getCompletionPercent() }} %
|
||||
|
@ -168,6 +281,9 @@
|
|||
<i class="bi bi-geo-alt"></i> Gouttes
|
||||
</button>
|
||||
</div>
|
||||
<button id="btn-geolocate" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-geo-alt"></i> Me localiser
|
||||
</button>
|
||||
</div>
|
||||
<div id="map" style="height: 400px; width: 100%; margin-bottom: 1rem;"></div>
|
||||
|
||||
|
@ -226,7 +342,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card mt-4" id="podium">
|
||||
<div class="card-header">
|
||||
<h2>Podium des contributeurs OSM de cette ville</h2>
|
||||
</div>
|
||||
|
@ -249,7 +365,7 @@
|
|||
<tr>
|
||||
<th scope="row">{{ loop.index }}</th>
|
||||
<td>
|
||||
<a href="https://www.openstreetmap.org/user/{{ row.osm_user|e('url') }}" target="_blank">
|
||||
<a href="https://www.openstreetmap.org/user/{{ row.osm_user|e('url') }}" >
|
||||
{{ row.osm_user }}
|
||||
</a>
|
||||
</td>
|
||||
|
@ -314,6 +430,7 @@
|
|||
<li>Site web</li>
|
||||
<li>Numéro de téléphone</li>
|
||||
<li>Accessibilité PMR</li>
|
||||
<li>SIRET</li>
|
||||
</ul>
|
||||
<p>Chaque critère rempli augmente le score de complétion d'une part égale.
|
||||
Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||
|
@ -326,39 +443,7 @@
|
|||
<h2 class="accordion-header" id="headingOne">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bouton caché pour JOSM -->
|
||||
<a id="josmButton" style="display: none;"></a>
|
||||
|
||||
{% set followup_icons = {
|
||||
'fire_hydrant': 'bi-droplet',
|
||||
'charging_station': 'bi-lightning-charge',
|
||||
'toilets': 'bi-toilet',
|
||||
'bus_stop': 'bi-bus-front',
|
||||
'defibrillator': 'bi-heart-pulse',
|
||||
'camera': 'bi-camera-video',
|
||||
'recycling': 'bi-recycle',
|
||||
'substation': 'bi-plug',
|
||||
'laboratory': 'bi-beaker',
|
||||
'school': 'bi-mortarboard',
|
||||
'places': 'bi-geo-alt'
|
||||
} %}
|
||||
|
||||
<div class="row mb-4">
|
||||
{% for type, data in latestFollowups %}
|
||||
{% if data.count or data.completion %}
|
||||
<div class="col-auto mb-2">
|
||||
<div class="card shadow-sm text-center" style="min-width: 140px;">
|
||||
<div class="card-body p-2">
|
||||
<i class="bi {{ followup_icons[type]|default('bi-question-circle') }} fs-2 mb-1"></i><br>
|
||||
<span class="fw-bold">{{ type_labels[type]|default(type|capitalize) }}</span><br>
|
||||
<span title="Nombre">N = {{ data.count ? data.count.getMeasure() : '?' }}</span><br>
|
||||
<span title="Complétion">Compl. = {{ data.completion ? data.completion.getMeasure() : '?' }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
@ -509,7 +594,7 @@
|
|||
if (properties.address) popupContent += `${properties.address}<br>`;
|
||||
if (properties.main_tag) popupContent += `<em>${properties.main_tag}</em><br>`;
|
||||
if (properties.note) popupContent += `<small>Note: ${properties.note}</small><br>`;
|
||||
popupContent += `<a href="${properties.osm_url}" target="_blank">Voir sur OSM</a>`;
|
||||
popupContent += `<a href="${properties.osm_url}" >Voir sur OSM</a>`;
|
||||
|
||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
||||
|
@ -539,11 +624,26 @@
|
|||
const openInJOSMButton = document.getElementById('openInJOSM');
|
||||
if (openInJOSMButton) {
|
||||
openInJOSMButton.addEventListener('click', () => {
|
||||
const osmElements = geojsonData.features.map(f => ({
|
||||
osm_id: f.properties.id.split('/')[1],
|
||||
osm_type: f.properties.id.split('/')[0]
|
||||
}));
|
||||
openInJOSM(map, map_is_loaded, osmElements);
|
||||
const place_nodes = [];
|
||||
const place_ways = [];
|
||||
const place_relations = [];
|
||||
const places = {{ geojson|raw }}.features;
|
||||
places.forEach(place => {
|
||||
if (place.properties.getOsmKind() === 'node') {
|
||||
place_nodes.push(place.properties.id.split('/')[1]);
|
||||
} elseif (place.properties.getOsmKind() === 'way') {
|
||||
place_ways.push(place.properties.id.split('/')[1]);
|
||||
} elseif (place.properties.getOsmKind() === 'relation') {
|
||||
place_relations.push(place.properties.id.split('/')[1]);
|
||||
}
|
||||
});
|
||||
const overpass_josm_query = '[out:xml][timeout:60];\n' +
|
||||
(place_nodes.length > 0 ? 'node(id:' + place_nodes.join(',') + ');\n' : '') +
|
||||
(place_ways.length > 0 ? 'way(id:' + place_ways.join(',') + ');\n' : '') +
|
||||
(place_relations.length > 0 ? 'relation(id:' + place_relations.join(',') + ');\n' : '') +
|
||||
'(._;>;);\nout meta;';
|
||||
const url = 'http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(overpass_josm_query);
|
||||
openInJOSM(map, map_is_loaded, [{osm_id: place_nodes.join(','), osm_type: 'node'}, {osm_id: place_ways.join(','), osm_type: 'way'}, {osm_id: place_relations.join(','), osm_type: 'relation'}], url);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -675,8 +775,10 @@ if(dc ){
|
|||
}
|
||||
new Chart(completionCtx, {
|
||||
type: 'line',
|
||||
|
||||
data: {
|
||||
labels: completionLabels,
|
||||
tension: 0.3,
|
||||
datasets: [{
|
||||
label: 'Distribution du Taux de Complétion',
|
||||
data: completionValues,
|
||||
|
@ -734,4 +836,49 @@ if(dc ){
|
|||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btn = document.getElementById('btn-geolocate');
|
||||
btn && btn.addEventListener('click', function() {
|
||||
if (!navigator.geolocation) {
|
||||
alert('La géolocalisation n\'est pas supportée par ce navigateur.');
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Localisation...';
|
||||
navigator.geolocation.getCurrentPosition(function(pos) {
|
||||
const lat = pos.coords.latitude;
|
||||
const lon = pos.coords.longitude;
|
||||
// MapLibre
|
||||
if (window.mapInstance && typeof window.mapInstance.flyTo === 'function') {
|
||||
window.mapInstance.flyTo({center: [lon, lat], zoom: 15});
|
||||
if (window._geoMarker) window.mapInstance.removeLayer('geo-marker');
|
||||
if (window._geoMarkerSource) window.mapInstance.removeSource('geo-marker');
|
||||
window.mapInstance.addSource('geo-marker', {
|
||||
type: 'geojson',
|
||||
data: { type: 'Feature', geometry: { type: 'Point', coordinates: [lon, lat] } }
|
||||
});
|
||||
window.mapInstance.addLayer({
|
||||
id: 'geo-marker',
|
||||
type: 'circle',
|
||||
source: 'geo-marker',
|
||||
paint: { 'circle-radius': 10, 'circle-color': '#007bff', 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' }
|
||||
});
|
||||
window._geoMarker = true;
|
||||
window._geoMarkerSource = true;
|
||||
} else if (window.L && window.map) { // Leaflet
|
||||
window.map.setView([lat, lon], 15);
|
||||
if (window._geoMarker) window.map.removeLayer(window._geoMarker);
|
||||
window._geoMarker = window.L.marker([lat, lon], {icon: window.L.icon({iconUrl: 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/icons/geo-alt-fill.svg', iconSize: [32,32]})}).addTo(window.map);
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-geo-alt"></i> Me localiser';
|
||||
}, function(err) {
|
||||
alert('Impossible de vous localiser : ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-geo-alt"></i> Me localiser';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue