mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
up historique
This commit is contained in:
parent
ad4170db14
commit
c274fd6a63
12 changed files with 448 additions and 616 deletions
|
@ -54,7 +54,7 @@
|
|||
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 0}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<button id="openInJOSM" class="btn btn-secondary ms-2">
|
||||
<i class="bi bi-map"></i> Ouvrir dans JOSM
|
||||
</button>
|
||||
|
@ -83,7 +83,7 @@
|
|||
{% endif %}
|
||||
|
||||
{# Affichage de la fraîcheur des données OSM #}
|
||||
{% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %}
|
||||
{# {% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %}
|
||||
{% set now = "now"|date("U") %}
|
||||
{% set minDate = stats.osmDataDateMin|date("U") %}
|
||||
{% set maxDate = stats.osmDataDateMax|date("U") %}
|
||||
|
@ -147,7 +147,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %} #}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-12">
|
||||
|
@ -249,14 +249,14 @@
|
|||
<h2>Requête Overpass</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="p-4 bg-light">
|
||||
{{query_places|raw}}
|
||||
</pre>
|
||||
<a href="https://overpass-turbo.eu/?Q={{ query_places|url_encode }}" class="btn btn-primary" target="_blank">
|
||||
<pre class="p-4 bg-light">
|
||||
{{ overpass_query|raw }}
|
||||
</pre>
|
||||
<a href="{{ overpass_query_url }}" class="btn btn-primary" target="_blank">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Exécuter dans Overpass Turbo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="history">
|
||||
<h2>Historique des {{ statsHistory|length }} stats</h2>
|
||||
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
|
||||
|
@ -305,18 +305,36 @@
|
|||
<div id="completionInfoContent" style="display: none;" class="mt-3">
|
||||
<p>Le score de complétion est calculé en fonction de plusieurs critères :</p>
|
||||
<ul>
|
||||
<li>Nom du commerce (obligatoire)</li>
|
||||
<li>Nom du commerce</li>
|
||||
<li>Adresse complète (numéro, rue, code postal)</li>
|
||||
<li>Horaires d'ouverture</li>
|
||||
<li>Site web</li>
|
||||
<li>Numéro de téléphone</li>
|
||||
<li>Accessibilité PMR</li>
|
||||
<li>Note descriptive</li>
|
||||
</ul>
|
||||
<p>Chaque critère rempli augmente le score de complétion. Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-calendar-event"></i> Fréquence des mises à jour (par trimestre)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="modificationsByQuarterChart" style="min-height: 250px; width: 100%;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion mb-3" id="accordionStats">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bouton caché pour JOSM -->
|
||||
<a id="josmButton" style="display: none;"></a>
|
||||
|
@ -326,9 +344,11 @@
|
|||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
||||
<script>
|
||||
<script type="module">
|
||||
|
||||
|
||||
// Attendre que le DOM et tous les scripts soient chargés
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Vérifier que Chart.js est disponible
|
||||
|
@ -351,22 +371,27 @@
|
|||
let selectedFeature = null;
|
||||
|
||||
// Fonction pour calculer la distribution des taux de complétion
|
||||
function calculateCompletionDistribution(features) {
|
||||
function calculateCompletionDistribution(elements) {
|
||||
const buckets = Array(11).fill(0); // 0-10%, 11-20%, ..., 91-100%
|
||||
|
||||
features.forEach(feature => {
|
||||
const completion = calculateCompletion(feature.properties);
|
||||
const bucketIndex = Math.min(Math.floor(completion.percentage / 10), 10);
|
||||
buckets[bucketIndex]++;
|
||||
elements.forEach(element => {
|
||||
if (element.tags) {
|
||||
const completion = calculateCompletion(element.tags);
|
||||
const bucketIndex = Math.min(Math.floor(completion.percentage / 10), 10);
|
||||
buckets[bucketIndex]++;
|
||||
}
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
// Fonction pour créer le graphique de complétion
|
||||
function createCompletionChart(features) {
|
||||
const ctx = document.getElementById('completionChart').getContext('2d');
|
||||
const distribution = calculateCompletionDistribution(features);
|
||||
function createCompletionChart(elements) {
|
||||
const chartElement = document.getElementById('completionChart');
|
||||
if (!chartElement) return;
|
||||
|
||||
const ctx = chartElement.getContext('2d');
|
||||
const distribution = calculateCompletionDistribution(elements);
|
||||
|
||||
if (completionChart) {
|
||||
completionChart.destroy();
|
||||
|
@ -401,19 +426,19 @@
|
|||
// Fonction pour charger les lieux depuis l'API Overpass
|
||||
async function loadPlaces() {
|
||||
try {
|
||||
const response = await fetch(`https://overpass-api.de/api/interpreter?data={{query_places|raw}}`);
|
||||
const data = await response.json();
|
||||
const response = await fetch(`https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode|raw }}`);
|
||||
const geojsonData = await response.json();
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
// Mettre à jour les statistiques
|
||||
const totallieux = data.features.length;
|
||||
document.getElementById('totallieux').textContent = totallieux;
|
||||
if (geojsonData.elements && geojsonData.elements.length > 0) {
|
||||
const totallieux = geojsonData.elements.length;
|
||||
const totallieuxElement = document.getElementById('totallieux');
|
||||
if (totallieuxElement) {
|
||||
totallieuxElement.textContent = totallieux;
|
||||
}
|
||||
|
||||
// Calculer et afficher la distribution des taux de complétion
|
||||
createCompletionChart(data.features);
|
||||
createCompletionChart(geojsonData.elements);
|
||||
|
||||
// Mettre à jour les marqueurs sur la carte
|
||||
dropMarkers = updateMarkers(data.features, map, currentMarkerType, dropMarkers, data);
|
||||
dropMarkers = updateMarkers(geojsonData.elements, map, currentMarkerType, dropMarkers, geojsonData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des lieux:', error);
|
||||
|
@ -477,6 +502,57 @@
|
|||
|
||||
// Charger les lieux au démarrage
|
||||
loadPlaces();
|
||||
|
||||
// Graphique des modifications par trimestre
|
||||
const modificationsData = JSON.parse('{{ modificationsByQuarter|raw }}');
|
||||
const quarterLabels = Object.keys(modificationsData);
|
||||
const quarterValues = Object.values(modificationsData);
|
||||
console.log('modificationsData', modificationsData);
|
||||
console.log('quarterLabels', quarterLabels);
|
||||
console.log('quarterValues', quarterValues);
|
||||
|
||||
if (quarterLabels.length > 0) {
|
||||
const chartElement = document.getElementById('modificationsByQuarterChart');
|
||||
if (chartElement) {
|
||||
const ctxQuarter = chartElement.getContext('2d');
|
||||
new Chart(ctxQuarter, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: quarterLabels,
|
||||
datasets: [{
|
||||
label: 'Nombre de modifications',
|
||||
data: quarterValues,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Canvas #modificationsByQuarterChart introuvable dans le DOM');
|
||||
}
|
||||
} else {
|
||||
console.warn('Aucune donnée de trimestre à afficher pour le graphique.');
|
||||
const chartElement = document.getElementById('modificationsByQuarterChart');
|
||||
if (chartElement) {
|
||||
chartElement.parentNode.innerHTML += '<div class="text-muted p-3">Aucune donnée de modification disponible pour cette zone.</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -494,12 +570,12 @@
|
|||
let contextMenu = null; // Menu contextuel
|
||||
|
||||
|
||||
function calculateCompletion(element) {
|
||||
let completionCount = 0;
|
||||
let totalFields = 0;
|
||||
function calculateCompletion(element) {
|
||||
let completionCount = 0;
|
||||
let totalFields = 0;
|
||||
let missingFields = [];
|
||||
|
||||
const fieldsToCheck = [
|
||||
|
||||
const fieldsToCheck = [
|
||||
{ name: 'name', label: 'Nom du commerce' },
|
||||
{ name: 'contact:street', label: 'Rue' },
|
||||
{ name: 'contact:housenumber', label: 'Numéro' },
|
||||
|
@ -507,12 +583,12 @@ function calculateCompletion(element) {
|
|||
{ name: 'contact:website', label: 'Site web' },
|
||||
{ name: 'contact:phone', label: 'Téléphone' },
|
||||
{ name: 'wheelchair', label: 'Accessibilité PMR' }
|
||||
];
|
||||
];
|
||||
|
||||
fieldsToCheck.forEach(field => {
|
||||
totalFields++;
|
||||
fieldsToCheck.forEach(field => {
|
||||
totalFields++;
|
||||
if (element.tags && element.tags[field.name]) {
|
||||
completionCount++;
|
||||
completionCount++;
|
||||
} else {
|
||||
missingFields.push(field.label);
|
||||
}
|
||||
|
@ -537,10 +613,10 @@ function getCompletionColor(completion) {
|
|||
}
|
||||
|
||||
|
||||
function createPopupContent(element) {
|
||||
function createPopupContent(element) {
|
||||
const completion = calculateCompletion(element);
|
||||
let content = `
|
||||
<div class="mb-2">
|
||||
let content = `
|
||||
<div class="mb-2">
|
||||
<h5>${element.tags?.name || 'Sans nom'}</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/placeType/${element.type}/${element.id}">
|
||||
|
@ -550,9 +626,9 @@ function createPopupContent(element) {
|
|||
<i class="bi bi-map"></i> OSM
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (completion.percentage < 100) {
|
||||
content += `
|
||||
<div class="alert alert-warning mt-2">
|
||||
|
@ -565,17 +641,17 @@ function createPopupContent(element) {
|
|||
}
|
||||
|
||||
content += '<table class="table table-sm mt-2">';
|
||||
|
||||
// Ajouter tous les tags
|
||||
if (element.tags) {
|
||||
for (const tag in element.tags) {
|
||||
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter tous les tags
|
||||
if (element.tags) {
|
||||
for (const tag in element.tags) {
|
||||
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
|
||||
content += '</table>';
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
content += '</table>';
|
||||
return content;
|
||||
}
|
||||
|
||||
function updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData) {
|
||||
// Supprimer tous les marqueurs existants
|
||||
|
@ -822,7 +898,7 @@ window.updateMarkers = updateMarkers;
|
|||
const tempLink = document.createElement('a');
|
||||
tempLink.style.display = 'none';
|
||||
document.body.appendChild(tempLink);
|
||||
|
||||
|
||||
tempLink.href = josmUrl;
|
||||
tempLink.click();
|
||||
document.body.removeChild(tempLink);
|
||||
|
@ -908,7 +984,7 @@ window.updateMarkers = updateMarkers;
|
|||
document.getElementById('openInJOSM').addEventListener('click', openInJOSM);
|
||||
|
||||
// Attendre que la carte soit chargée avant d'ajouter les écouteurs d'événements
|
||||
map.on('load', function() {
|
||||
map.on('load', function() {
|
||||
map_is_loaded = true;
|
||||
// Changer le curseur au survol des marqueurs
|
||||
map.on('mouseenter', function(e) {
|
||||
|
@ -984,6 +1060,7 @@ window.updateMarkers = updateMarkers;
|
|||
icon.classList.add('bi-chevron-down');
|
||||
}
|
||||
}
|
||||
window.toggleCompletionInfo = toggleCompletionInfo;
|
||||
|
||||
|
||||
// infos depuis complète tes commerces : CTC
|
||||
|
@ -1102,7 +1179,6 @@ function makeDonutGraphOfTags() {
|
|||
labels: {
|
||||
boxWidth: 15,
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
|
@ -1115,10 +1191,11 @@ function makeDonutGraphOfTags() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
makeDonutGraphOfTags();
|
||||
markClosedSiretsOnTable();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -117,8 +117,8 @@
|
|||
|
||||
<div class="small osm-modification-info">
|
||||
<div>
|
||||
{{ commerce.osmDataDate|date('Y-m-d H:i') }}
|
||||
<i class="bi bi-calendar"></i>
|
||||
{{ commerce.osmDataDate|date('d/m/Y H:i') }}
|
||||
</div>
|
||||
{% if commerce.osmUser %}
|
||||
<div>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
Statistiques des villes (nombre de commerces)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="statsBubbleChart" style="min-height: 400px; width: 100%;"></canvas>
|
||||
<canvas id="statsBubbleChart" style="min-height: 400px; width: 100%;" data-stats="{{ stats|raw }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,7 +110,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in stats %}
|
||||
{% for stat in stats_list %}
|
||||
<tr>
|
||||
<td><a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" title="Voir les statistiques de cette ville">
|
||||
{{ stat.name }}
|
||||
|
@ -121,8 +121,8 @@
|
|||
</a></td>
|
||||
<td>{{ stat.zone }}</td>
|
||||
<td>{{ stat.completionPercent }}%</td>
|
||||
<td>{{ stat.placesCount }}</td>
|
||||
<td>{{ (stat.placesCount / (stat.population or 1 ))|round(2) }}</td>
|
||||
<td>{{ stat.places|length }}</td>
|
||||
<td>{{ (stat.places|length / (stat.population or 1 ))|round(2) }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" class="btn btn-sm btn-primary" title="Voir les statistiques de cette ville">
|
||||
|
@ -158,113 +158,5 @@
|
|||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{# Les scripts sont maintenant gérés par Webpack Encore via app.js #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const statsDataRaw = [
|
||||
{% for stat in stats %}
|
||||
{% if stat.placesCount > 0 and stat.name is not null and stat.population > 0 %}
|
||||
{
|
||||
label: '{{ (stat.name ~ " (" ~ stat.zone ~ ")")|e('js') }}',
|
||||
placesCount: {{ stat.placesCount }},
|
||||
completion: {{ stat.completionPercent|default(0) }},
|
||||
x: {{ stat.population }},
|
||||
y: {{ (stat.placesCount / stat.population * 1000)|round(2) }}
|
||||
},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const ctx = document.getElementById('statsBubbleChart');
|
||||
if (ctx && statsDataRaw.length > 0) {
|
||||
const statsData = statsDataRaw.map(d => ({
|
||||
...d,
|
||||
r: Math.sqrt(d.placesCount) * 2.5 // Utilise la racine carrée pour la taille, avec un facteur d'échelle
|
||||
}));
|
||||
|
||||
new Chart(ctx.getContext('2d'), {
|
||||
type: 'bubble',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Commerces par ville',
|
||||
data: statsData,
|
||||
backgroundColor: statsData.map(d => `hsla(120, 60%, 70%, ${d.completion / 120 + 0.2})`),
|
||||
borderColor: 'hsl(120, 60%, 40%)',
|
||||
borderWidth: 1,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const data = context.dataset.data[context.dataIndex];
|
||||
let label = data.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += `${data.placesCount} commerces, ${data.y} pour 1000 hab., ${data.completion}% complétion`;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'logarithmic',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Population (échelle log)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Commerces pour 1000 habitants'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// La fonction est maintenant globale grâce à l'import dans app.js
|
||||
if (typeof colorizePercentageCells === 'function') {
|
||||
colorizePercentageCells('td:nth-child(3)');
|
||||
}
|
||||
|
||||
// Gérer le formulaire de labourage
|
||||
const labourageForm = document.getElementById('labourerForm');
|
||||
const citySearchInput = document.getElementById('citySearch');
|
||||
const selectedZipCodeInput = document.getElementById('selectedZipCode');
|
||||
const labourageBtn = labourageForm.querySelector('button[type="submit"]');
|
||||
const originalBtnHtml = labourageBtn.innerHTML;
|
||||
|
||||
if (labourageForm && citySearchInput && typeof setupCitySearch === 'function') {
|
||||
setupCitySearch('citySearch', 'citySuggestions', function (suggestion) {
|
||||
// Afficher le spinner et désactiver le bouton
|
||||
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Chargement...';
|
||||
labourageBtn.disabled = true;
|
||||
citySearchInput.disabled = true;
|
||||
|
||||
if (suggestion.insee) {
|
||||
window.location.href = `/admin/labourer/${suggestion.insee}`;
|
||||
} else if (suggestion.postcode) {
|
||||
// Moins probable, mais en solution de repli
|
||||
window.location.href = `/admin/labourer/${suggestion.postcode}`;
|
||||
}
|
||||
});
|
||||
|
||||
labourageForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
alert("Veuillez rechercher et sélectionner une ville directement dans la liste de suggestions.");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{# Le script du graphique est maintenant dans assets/app.js #}
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue