mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
suivi global
This commit is contained in:
parent
afc120ef2a
commit
e3f8680472
6 changed files with 531 additions and 48 deletions
|
@ -13,7 +13,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||
|
||||
class FollowUpController extends AbstractController
|
||||
{
|
||||
#[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete')]
|
||||
#[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete', requirements: ['insee_code' => '\\d+'])]
|
||||
public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response {
|
||||
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if (!$stats) {
|
||||
|
@ -29,7 +29,7 @@ class FollowUpController extends AbstractController
|
|||
return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]);
|
||||
}
|
||||
|
||||
#[Route('/admin/followup/{insee_code}', name: 'admin_followup')]
|
||||
#[Route('/admin/followup/{insee_code}', name: 'admin_followup', requirements: ['insee_code' => '\\d+'])]
|
||||
public function followup(
|
||||
string $insee_code,
|
||||
Motocultrice $motocultrice,
|
||||
|
@ -93,6 +93,21 @@ class FollowUpController extends AbstractController
|
|||
'label' => 'Écoles',
|
||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school')
|
||||
],
|
||||
'police' => [
|
||||
'label' => 'Commissariats',
|
||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police')
|
||||
],
|
||||
'healthcare' => [
|
||||
'label' => 'Lieux de santé',
|
||||
'objects' => array_filter($elements, function($el) {
|
||||
return isset($el['tags']['healthcare'])
|
||||
|| ($el['tags']['amenity'] ?? null) === 'doctors'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'pharmacy'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'hospital'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'clinic'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'social_facility';
|
||||
})
|
||||
],
|
||||
];
|
||||
$now = new \DateTime();
|
||||
foreach ($types as $type => $data) {
|
||||
|
@ -146,6 +161,19 @@ class FollowUpController extends AbstractController
|
|||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['ref:UAI'] ?? null) && !empty($el['tags']['isced:level'] ?? null) && !empty($el['tags']['school:FR'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'police') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['phone'] ?? null) || !empty($el['tags']['website'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'healthcare') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['contact:phone'] ?? null)
|
||||
|| !empty($tags['phone'] ?? null)
|
||||
|| !empty($tags['email'] ?? null)
|
||||
|| !empty($tags['contact:email'] ?? null);
|
||||
});
|
||||
}
|
||||
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||
$followupCompletion = new CityFollowUp();
|
||||
|
@ -161,7 +189,7 @@ class FollowUpController extends AbstractController
|
|||
return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]);
|
||||
}
|
||||
|
||||
#[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph')]
|
||||
#[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph', requirements: ['insee_code' => '\\d+'])]
|
||||
public function followupGraph(
|
||||
string $insee_code,
|
||||
EntityManagerInterface $em,
|
||||
|
@ -219,6 +247,21 @@ class FollowUpController extends AbstractController
|
|||
'label' => 'Écoles',
|
||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school')
|
||||
],
|
||||
'police' => [
|
||||
'label' => 'Commissariats',
|
||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police')
|
||||
],
|
||||
'healthcare' => [
|
||||
'label' => 'Lieux de santé',
|
||||
'objects' => array_filter($elements, function($el) {
|
||||
return isset($el['tags']['healthcare'])
|
||||
|| ($el['tags']['amenity'] ?? null) === 'doctors'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'pharmacy'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'hospital'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'clinic'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'social_facility';
|
||||
})
|
||||
],
|
||||
];
|
||||
$now = new \DateTime();
|
||||
foreach ($types as $type => $data) {
|
||||
|
@ -270,6 +313,19 @@ class FollowUpController extends AbstractController
|
|||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['ref:UAI'] ?? null) && !empty($el['tags']['isced:level'] ?? null) && !empty($el['tags']['school:FR'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'police') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['phone'] ?? null) || !empty($el['tags']['website'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'healthcare') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['contact:phone'] ?? null)
|
||||
|| !empty($tags['phone'] ?? null)
|
||||
|| !empty($tags['email'] ?? null)
|
||||
|| !empty($tags['contact:email'] ?? null);
|
||||
});
|
||||
}
|
||||
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||
$followupCompletion = new CityFollowUp();
|
||||
|
@ -356,6 +412,21 @@ class FollowUpController extends AbstractController
|
|||
'label' => 'Écoles',
|
||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school')
|
||||
],
|
||||
'police' => [
|
||||
'label' => 'Commissariats',
|
||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police')
|
||||
],
|
||||
'healthcare' => [
|
||||
'label' => 'Lieux de santé',
|
||||
'objects' => array_filter($elements, function($el) {
|
||||
return isset($el['tags']['healthcare'])
|
||||
|| ($el['tags']['amenity'] ?? null) === 'doctors'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'pharmacy'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'hospital'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'clinic'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'social_facility';
|
||||
})
|
||||
],
|
||||
];
|
||||
foreach ($types as $type => $data) {
|
||||
// Suivi du nombre
|
||||
|
@ -406,6 +477,19 @@ class FollowUpController extends AbstractController
|
|||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['ref:UAI'] ?? null) && !empty($el['tags']['isced:level'] ?? null) && !empty($el['tags']['school:FR'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'police') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
return !empty($el['tags']['phone'] ?? null) || !empty($el['tags']['website'] ?? null);
|
||||
});
|
||||
} elseif ($type === 'healthcare') {
|
||||
$completed = array_filter($data['objects'], function($el) {
|
||||
$tags = $el['tags'] ?? [];
|
||||
return !empty($tags['name'] ?? null)
|
||||
|| !empty($tags['contact:phone'] ?? null)
|
||||
|| !empty($tags['phone'] ?? null)
|
||||
|| !empty($tags['email'] ?? null)
|
||||
|| !empty($tags['contact:email'] ?? null);
|
||||
});
|
||||
}
|
||||
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||
$followupCompletion = new CityFollowUp();
|
||||
|
@ -434,4 +518,128 @@ class FollowUpController extends AbstractController
|
|||
$this->addFlash('success', 'Suivi généré pour toutes les villes.');
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
|
||||
#[Route('/admin/followup/global', name: 'admin_followup_global')]
|
||||
public function followupGlobal(EntityManagerInterface $em) {
|
||||
// Récupérer ou créer l'objet Stats global
|
||||
$statsGlobal = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']);
|
||||
if (!$statsGlobal) {
|
||||
$statsGlobal = new Stats();
|
||||
$statsGlobal->setZone('00000');
|
||||
$statsGlobal->setName('toutes les villes');
|
||||
$em->persist($statsGlobal);
|
||||
$em->flush();
|
||||
}
|
||||
$now = new \DateTime();
|
||||
$themes = [
|
||||
'fire_hydrant', 'charging_station', 'toilets', 'bus_stop', 'defibrillator', 'camera', 'recycling', 'substation', 'laboratory', 'school', 'police', 'healthcare', 'places'
|
||||
];
|
||||
$allStats = $em->getRepository(Stats::class)->findAll();
|
||||
// Exclure l'objet global
|
||||
$allStats = array_filter($allStats, fn($s) => $s->getZone() !== '00000');
|
||||
$cityCount = count($allStats);
|
||||
$globalCompletionSum = 0;
|
||||
$globalCompletionCount = 0;
|
||||
// Pour chaque thème, somme des counts et moyenne des completions
|
||||
foreach ($themes as $theme) {
|
||||
$sumCount = 0;
|
||||
$sumCompletion = 0;
|
||||
$nbCompletion = 0;
|
||||
foreach ($allStats as $stats) {
|
||||
$latestCount = null;
|
||||
$latestCompletion = null;
|
||||
foreach ($stats->getCityFollowUps() as $fu) {
|
||||
if ($fu->getName() === $theme . '_count') {
|
||||
if ($latestCount === null || $fu->getDate() > $latestCount->getDate()) {
|
||||
$latestCount = $fu;
|
||||
}
|
||||
}
|
||||
if ($fu->getName() === $theme . '_completion') {
|
||||
if ($latestCompletion === null || $fu->getDate() > $latestCompletion->getDate()) {
|
||||
$latestCompletion = $fu;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($latestCount) $sumCount += $latestCount->getMeasure();
|
||||
if ($latestCompletion) {
|
||||
$sumCompletion += $latestCompletion->getMeasure();
|
||||
$nbCompletion++;
|
||||
}
|
||||
}
|
||||
// Ajout du CityFollowUp global pour le count
|
||||
$fuCount = new CityFollowUp();
|
||||
$fuCount->setName($theme . '_count')
|
||||
->setMeasure($sumCount)
|
||||
->setDate($now)
|
||||
->setStats($statsGlobal);
|
||||
$em->persist($fuCount);
|
||||
// Ajout du CityFollowUp global pour la complétion
|
||||
$completionAvg = $nbCompletion > 0 ? round($sumCompletion / $nbCompletion, 1) : 0;
|
||||
$fuCompletion = new CityFollowUp();
|
||||
$fuCompletion->setName($theme . '_completion')
|
||||
->setMeasure($completionAvg)
|
||||
->setDate($now)
|
||||
->setStats($statsGlobal);
|
||||
$em->persist($fuCompletion);
|
||||
// Pour la complétion globale moyenne
|
||||
if ($theme !== 'places' && $nbCompletion > 0) {
|
||||
$globalCompletionSum += $completionAvg;
|
||||
$globalCompletionCount++;
|
||||
}
|
||||
}
|
||||
// Suivi du nombre total de lieux (places_count)
|
||||
$sumPlaces = 0;
|
||||
foreach ($allStats as $stats) {
|
||||
$sumPlaces += $stats->getPlacesCount() ?? 0;
|
||||
}
|
||||
$fuPlaces = new CityFollowUp();
|
||||
$fuPlaces->setName('places_count')
|
||||
->setMeasure($sumPlaces)
|
||||
->setDate($now)
|
||||
->setStats($statsGlobal);
|
||||
$em->persist($fuPlaces);
|
||||
// Suivi de la complétion globale moyenne
|
||||
$globalCompletionAvg = $globalCompletionCount > 0 ? round($globalCompletionSum / $globalCompletionCount, 1) : 0;
|
||||
$fuGlobalCompletion = new CityFollowUp();
|
||||
$fuGlobalCompletion->setName('global_completion_average')
|
||||
->setMeasure($globalCompletionAvg)
|
||||
->setDate($now)
|
||||
->setStats($statsGlobal);
|
||||
$em->persist($fuGlobalCompletion);
|
||||
// Suivi du nombre de villes
|
||||
$fuCityCount = new CityFollowUp();
|
||||
$fuCityCount->setName('city_count')
|
||||
->setMeasure($cityCount)
|
||||
->setDate($now)
|
||||
->setStats($statsGlobal);
|
||||
$em->persist($fuCityCount);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Suivi global généré pour toutes les villes.');
|
||||
return $this->redirectToRoute('admin_followup_global_graph');
|
||||
}
|
||||
|
||||
#[Route('/admin/followup/global/graph', name: 'admin_followup_global_graph')]
|
||||
public function followupGlobalGraph(EntityManagerInterface $em) {
|
||||
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']);
|
||||
if (!$stats) {
|
||||
$this->addFlash('error', 'Aucun suivi global trouvé.');
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
$followups = $stats->getCityFollowUps();
|
||||
$followups = $followups->toArray();
|
||||
usort($followups, fn($a, $b) => $a->getDate() <=> $b->getDate());
|
||||
// Grouper par type
|
||||
$series = [];
|
||||
foreach ($followups as $fu) {
|
||||
$series[$fu->getName()][] = [
|
||||
'date' => $fu->getDate()->format('c'),
|
||||
'value' => $fu->getMeasure(),
|
||||
'name' => $fu->getName(),
|
||||
];
|
||||
}
|
||||
return $this->render('admin/followup_global_graph.html.twig', [
|
||||
'stats' => $stats,
|
||||
'series' => $series
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -573,12 +573,19 @@ area["ref:INSEE"="$zone"]->.searchArea;
|
|||
nwr["man_made"="surveillance"](area.searchArea);
|
||||
nwr["amenity"="recycling"](area.searchArea);
|
||||
nwr["power"="substation"](area.searchArea);
|
||||
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);
|
||||
nwr["healthcare"="laboratory"](area.searchArea);
|
||||
nwr["amenity"="school"](area.searchArea);
|
||||
nwr["amenity"="police"](area.searchArea);
|
||||
);
|
||||
out body;
|
||||
(._;>;);
|
||||
out meta;
|
||||
>;
|
||||
out skel qt;
|
||||
QUERY;
|
||||
}
|
||||
|
||||
|
|
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 %}
|
||||
|
|
|
@ -47,6 +47,12 @@
|
|||
Podium des contributeurs OSM
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('admin_followup_global_graph') }}">
|
||||
<i class="bi bi-globe"></i>
|
||||
Suivi global OSM
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue