up pages rues et évolutions dans le temps

This commit is contained in:
Tykayn 2025-07-12 12:53:06 +02:00 committed by tykayn
parent 7355600e6b
commit c8e3cf2ada
10 changed files with 329 additions and 14 deletions

View file

@ -20,6 +20,54 @@
</a>
</div>
<p>Historique des objets suivis (nombre et complétion).</p>
<div id="objets_modified_lately">
{% set has_change = false %}
<table class="table table-bordered table-sm align-middle table-sortable" id="latestDiffsTable">
<thead>
<tr>
<th>Thème</th>
<th>Évolution du nombre</th>
<th>Évolution de la complétion</th>
</tr>
</thead>
<tbody>
{% for type, diff in latest_diffs %}
{% if diff.count_diff is not null and diff.count_diff != 0 %}
{% set has_change = true %}
<tr>
<td>
<a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': type}) }}" class="fw-bold text-decoration-none">
<i class="bi {{ followup_icons[type]|default('bi-question-circle') }}"></i> {{ diff.label }}
</a>
</td>
<td>
{% if diff.count_diff > 0 %}
<i class="bi bi-arrow-up text-success"></i>
{% else %}
<i class="bi bi-arrow-down text-danger"></i>
{% endif %}
{{ diff.count_diff > 0 ? '+' ~ diff.count_diff : diff.count_diff }} objets
</td>
<td>
{% if diff.completion_diff > 0 %}
<i class="bi bi-arrow-up text-success"></i>
{% elseif diff.completion_diff < 0 %}
<i class="bi bi-arrow-down text-danger"></i>
{% else %}
<i class="bi bi-arrow-right text-secondary"></i>
{% endif %}
{{ diff.completion_diff > 0 ? '+' ~ diff.completion_diff : diff.completion_diff }}%
</td>
</tr>
{% endif %}
{% endfor %}
{% if not has_change %}
<tr><td colspan="3" class="text-muted">Aucun changement significatif cette semaine.</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% for type, label in followup_labels %}
<h2 id="title-{{ type }}"><i class="bi {{ followup_icons[type]|default('bi-question-circle') }} fs-2"></i> {{ label }}</h2>
<canvas id="{{ type }}Chart" width="600" height="400"></canvas>
@ -41,6 +89,8 @@
'all_types': [type]
} %}
{% endfor %}
<h2 class="mt-4">Comparaison de la complétion par thème</h2>
<canvas id="multiCompletionChart" width="900" height="400"></canvas>
<h2 class="mt-4">Données brutes</h2>
<table class="table table-bordered table-striped">
<thead>
@ -71,10 +121,11 @@
<script src="/js/chartjs/chart.umd.js"></script>
<script src="/js/chartjs/chartjs-adapter-date-fns.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<script src="/assets/js/table-sortable.js"></script>
<script>
const series = {{ series|json_encode|raw }};
const followupIcons = {{ followup_icons|json_encode|raw }};
const typeLabels = Object.assign({}, {{ followup_labels|json_encode|raw }});
const typeLabels = Object.assign({}, {{ followup_labels|json_encode|raw }});
document.addEventListener('DOMContentLoaded', function() {
Object.keys(typeLabels).forEach(function(baseType) {
const countData = (series[baseType + '_count'] || []).map(pt => ({ x: pt.date, y: pt.value }));
@ -169,6 +220,7 @@
canvas.parentNode.insertBefore(dateDiv, canvas.nextSibling);
}
});
});
</script>
{% endblock %}

View file

@ -161,6 +161,12 @@
<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 %}

View file

@ -136,6 +136,9 @@
<a href="{{ path('app_public_stats_evolutions', {'insee_code': stats.zone}) }}" class="btn btn-outline-info ms-2">
<i class="bi bi-activity"></i> Évolutions des objets
</a>
<a href="{{ path('admin_street_completion', {'insee_code': stats.zone}) }}" class="btn btn-outline-success ms-2">
<i class="bi bi-signpost"></i> Complétion des rues
</a>
</div>
</div>
{% if stats.population %}

View file

@ -0,0 +1,152 @@
{% extends 'base.html.twig' %}
{% block title %}Complétion des rues - {{ stats.name }}{% endblock %}
{% block body %}
<div class="container my-5">
<h1>Complétion des rues à {{ stats.name }} ({{ stats.zone }})</h1>
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-secondary mb-3"><i class="bi bi-arrow-left"></i> Retour aux stats</a>
<table class="table table-bordered table-striped table-hover table-responsive">
<thead>
<tr>
<th>Rue</th>
<th class="text-end">Nombre de lieux</th>
<th class="text-end">Complétion moyenne (%)</th>
</tr>
</thead>
<tbody>
{% for rue in rues %}
<tr>
<td>
<a href="{{ path('app_public_street', {'cityId': insee_code, 'streetName': rue.name|url_encode }) }}">
{{ rue.name }}
</a>
</td>
<td class="text-end">{{ rue.count }}</td>
<td class="text-end">{{ rue.avg_completion }}</td>
</tr>
{% else %}
<tr><td colspan="3" class="text-muted">Aucune rue trouvée.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="missing-streets-block" class="mt-5">
<h2>Rues sans lieu associé (données OSM)</h2>
<div id="missing-streets-loading">Chargement des rues OSM…</div>
<ul id="missing-streets-list" class="list-group"></ul>
</div>
<script>
function buildOverpassAroundUrl(street, bbox) {
// Requête Overpass autour de la rue (10m)
// Utilise la requête de base de la motocultrice, mais avec around:10
// Ici on cherche les commerces autour de la géométrie de la rue
const query = `[out:json][timeout:25];
way["name"="${street}"]["highway"](${bbox.join(",")});
out body;
>;
out skel qt;
node(around:10)[shop](if:t["name"])(if:t["addr:street"]=="${street}");
out;`;
return `https://overpass-turbo.eu/map.html?Q=${encodeURIComponent(query)}`;
}
function buildOsmUrl(street, bbox) {
// Lien OSM pour la rue (zoom sur la bbox)
const [s, w, n, e] = bbox;
const lat = (parseFloat(s) + parseFloat(n)) / 2;
const lon = (parseFloat(w) + parseFloat(e)) / 2;
return `https://www.openstreetmap.org/#map=18/${lat}/${lon}`;
}
function fetchStreetBbox(street, insee, cb) {
// Requête Overpass pour trouver la bbox de la rue
const query = `[out:json][timeout:25];area["ref:INSEE"="${insee}"][admin_level=8];way["name"="${street}"]["highway"](area);out bb;`;
fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: query
})
.then(r => r.json())
.then(data => {
if (data.elements && data.elements.length > 0 && data.elements[0].bounds) {
const b = data.elements[0].bounds;
cb([b.south, b.west, b.north, b.east]);
}
});
}
document.addEventListener('DOMContentLoaded', function() {
// Récupérer la liste des rues déjà présentes dans le tableau
const ruesConnues = new Set([
{% for rue in rues %}'{{ rue.name|e('js') }}',{% endfor %}
]);
// Construire la requête Overpass pour toutes les rues de la ville
const insee = '{{ insee_code }}';
const overpassQuery = `
[out:json][timeout:25];
area["ref:INSEE"="${insee}"]->.searchArea;
way["highway"]["name"](area.searchArea);
out tags;
`;
fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: overpassQuery,
headers: { 'Content-Type': 'text/plain' }
})
.then(r => r.json())
.then(data => {
const allStreets = new Set();
(data.elements || []).forEach(el => {
if (el.tags && el.tags.name) {
allStreets.add(el.tags.name);
}
});
// Filtrer les rues qui n'ont pas de lieu associé
const missing = Array.from(allStreets).filter(nom => !ruesConnues.has(nom));
const ul = document.getElementById('missing-streets-list');
ul.innerHTML = '';
if (missing.length === 0) {
ul.innerHTML = '<li class="list-group-item text-success">Toutes les rues OSM ont au moins un lieu associé.</li>';
} else {
missing.sort((a, b) => a.localeCompare(b, 'fr'));
missing.forEach(nom => {
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center';
li.setAttribute('data-street-name', nom);
li.textContent = nom;
const buttons = document.createElement('span');
buttons.innerHTML = `
<a href="#" class="btn btn-outline-secondary btn-sm osm-link" title="Voir la rue sur OpenStreetMap" target="_blank" data-street="${nom}">
<i class="bi bi-globe"></i>
</a>
<a href="#" class="btn btn-outline-primary btn-sm overpass-link ms-1" title="Lieux possibles sur Overpass" target="_blank" data-street="${nom}">
<i class="bi bi-ev-station"></i>
</a>
`;
li.appendChild(buttons);
ul.appendChild(li);
});
}
document.getElementById('missing-streets-loading').style.display = 'none';
})
.catch(e => {
document.getElementById('missing-streets-loading').textContent = 'Erreur lors de la récupération des rues OSM.';
});
document.querySelectorAll('#missing-streets-list li').forEach(function(li) {
const street = li.getAttribute('data-street-name');
const insee = '{{ insee_code }}';
li.querySelector('.osm-link').addEventListener('click', function(e) {
e.preventDefault();
fetchStreetBbox(street, insee, function(bbox) {
window.open(buildOsmUrl(street, bbox), '_blank');
});
});
li.querySelector('.overpass-link').addEventListener('click', function(e) {
e.preventDefault();
fetchStreetBbox(street, insee, function(bbox) {
window.open(buildOverpassAroundUrl(street, bbox), '_blank');
});
});
});
});
</script>
{% endblock %}