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
|
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 {
|
public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response {
|
||||||
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
$stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||||
if (!$stats) {
|
if (!$stats) {
|
||||||
|
@ -29,7 +29,7 @@ class FollowUpController extends AbstractController
|
||||||
return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]);
|
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(
|
public function followup(
|
||||||
string $insee_code,
|
string $insee_code,
|
||||||
Motocultrice $motocultrice,
|
Motocultrice $motocultrice,
|
||||||
|
@ -93,6 +93,21 @@ class FollowUpController extends AbstractController
|
||||||
'label' => 'Écoles',
|
'label' => 'Écoles',
|
||||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school')
|
'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();
|
$now = new \DateTime();
|
||||||
foreach ($types as $type => $data) {
|
foreach ($types as $type => $data) {
|
||||||
|
@ -146,6 +161,19 @@ class FollowUpController extends AbstractController
|
||||||
$completed = array_filter($data['objects'], function($el) {
|
$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);
|
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;
|
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||||
$followupCompletion = new CityFollowUp();
|
$followupCompletion = new CityFollowUp();
|
||||||
|
@ -161,7 +189,7 @@ class FollowUpController extends AbstractController
|
||||||
return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]);
|
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(
|
public function followupGraph(
|
||||||
string $insee_code,
|
string $insee_code,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
|
@ -219,6 +247,21 @@ class FollowUpController extends AbstractController
|
||||||
'label' => 'Écoles',
|
'label' => 'Écoles',
|
||||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school')
|
'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();
|
$now = new \DateTime();
|
||||||
foreach ($types as $type => $data) {
|
foreach ($types as $type => $data) {
|
||||||
|
@ -270,6 +313,19 @@ class FollowUpController extends AbstractController
|
||||||
$completed = array_filter($data['objects'], function($el) {
|
$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);
|
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;
|
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||||
$followupCompletion = new CityFollowUp();
|
$followupCompletion = new CityFollowUp();
|
||||||
|
@ -356,6 +412,21 @@ class FollowUpController extends AbstractController
|
||||||
'label' => 'Écoles',
|
'label' => 'Écoles',
|
||||||
'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school')
|
'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) {
|
foreach ($types as $type => $data) {
|
||||||
// Suivi du nombre
|
// Suivi du nombre
|
||||||
|
@ -406,6 +477,19 @@ class FollowUpController extends AbstractController
|
||||||
$completed = array_filter($data['objects'], function($el) {
|
$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);
|
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;
|
$completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0;
|
||||||
$followupCompletion = new CityFollowUp();
|
$followupCompletion = new CityFollowUp();
|
||||||
|
@ -434,4 +518,128 @@ class FollowUpController extends AbstractController
|
||||||
$this->addFlash('success', 'Suivi généré pour toutes les villes.');
|
$this->addFlash('success', 'Suivi généré pour toutes les villes.');
|
||||||
return $this->redirectToRoute('app_admin');
|
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["man_made"="surveillance"](area.searchArea);
|
||||||
nwr["amenity"="recycling"](area.searchArea);
|
nwr["amenity"="recycling"](area.searchArea);
|
||||||
nwr["power"="substation"](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["healthcare"="laboratory"](area.searchArea);
|
||||||
nwr["amenity"="school"](area.searchArea);
|
nwr["amenity"="school"](area.searchArea);
|
||||||
|
nwr["amenity"="police"](area.searchArea);
|
||||||
);
|
);
|
||||||
out body;
|
(._;>;);
|
||||||
|
out meta;
|
||||||
>;
|
>;
|
||||||
out skel qt;
|
|
||||||
QUERY;
|
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 %}
|
{% block body %}
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h1>Suivi des objets OSM pour {{ stats.name }} ({{ stats.zone }})</h1>
|
<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">
|
<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)
|
<i class="bi bi-arrow-repeat"></i> Mettre à jour les suivis (followup)
|
||||||
</a>
|
</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>
|
</div>
|
||||||
<p>Historique des objets suivis (nombre et complétion).</p>
|
<p>Historique des objets suivis (nombre et complétion).</p>
|
||||||
{% set type_labels = {
|
{% set type_labels = {
|
||||||
|
@ -21,7 +27,9 @@
|
||||||
'recycling': 'Points de recyclage',
|
'recycling': 'Points de recyclage',
|
||||||
'substation': 'Sous-stations électriques',
|
'substation': 'Sous-stations électriques',
|
||||||
'laboratory': "Laboratoires d'analyse",
|
'laboratory': "Laboratoires d'analyse",
|
||||||
'school': 'Écoles'
|
'school': 'Écoles',
|
||||||
|
'police': 'Commissariats',
|
||||||
|
'healthcare': 'Lieux de santé'
|
||||||
} %}
|
} %}
|
||||||
{% for type in type_labels|keys %}
|
{% for type in type_labels|keys %}
|
||||||
<h2 id="title-{{ type }}">{{ type_labels[type] }}</h2>
|
<h2 id="title-{{ type }}">{{ type_labels[type] }}</h2>
|
||||||
|
@ -91,7 +99,9 @@
|
||||||
recycling: 'Points de recyclage',
|
recycling: 'Points de recyclage',
|
||||||
substation: 'Sous-stations électriques',
|
substation: 'Sous-stations électriques',
|
||||||
laboratory: "Laboratoires d'analyse",
|
laboratory: "Laboratoires d'analyse",
|
||||||
school: 'Écoles'
|
school: 'Écoles',
|
||||||
|
police: 'Commissariats',
|
||||||
|
healthcare: 'Lieux de santé'
|
||||||
};
|
};
|
||||||
function formatDelta(val) {
|
function formatDelta(val) {
|
||||||
if (val === null) return '-';
|
if (val === null) return '-';
|
||||||
|
|
|
@ -41,6 +41,27 @@
|
||||||
border-left: 4px solid #0dcaf0;
|
border-left: 4px solid #0dcaf0;
|
||||||
background-color: #f8f9fa;
|
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>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -107,6 +128,98 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<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">
|
<div class="col-md-3 col-12">
|
||||||
<span class="badge {% if stats.getCompletionPercent() > 85 %}bg-success{% else %}bg-warning{% endif %}">
|
<span class="badge {% if stats.getCompletionPercent() > 85 %}bg-success{% else %}bg-warning{% endif %}">
|
||||||
{{ stats.getCompletionPercent() }} %
|
{{ stats.getCompletionPercent() }} %
|
||||||
|
@ -168,6 +281,9 @@
|
||||||
<i class="bi bi-geo-alt"></i> Gouttes
|
<i class="bi bi-geo-alt"></i> Gouttes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="btn-geolocate" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="bi bi-geo-alt"></i> Me localiser
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="map" style="height: 400px; width: 100%; margin-bottom: 1rem;"></div>
|
<div id="map" style="height: 400px; width: 100%; margin-bottom: 1rem;"></div>
|
||||||
|
|
||||||
|
@ -226,7 +342,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-4">
|
<div class="card mt-4" id="podium">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Podium des contributeurs OSM de cette ville</h2>
|
<h2>Podium des contributeurs OSM de cette ville</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -249,7 +365,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ loop.index }}</th>
|
<th scope="row">{{ loop.index }}</th>
|
||||||
<td>
|
<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 }}
|
{{ row.osm_user }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -314,6 +430,7 @@
|
||||||
<li>Site web</li>
|
<li>Site web</li>
|
||||||
<li>Numéro de téléphone</li>
|
<li>Numéro de téléphone</li>
|
||||||
<li>Accessibilité PMR</li>
|
<li>Accessibilité PMR</li>
|
||||||
|
<li>SIRET</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Chaque critère rempli augmente le score de complétion d'une part égale.
|
<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>
|
Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||||
|
@ -326,39 +443,7 @@
|
||||||
<h2 class="accordion-header" id="headingOne">
|
<h2 class="accordion-header" id="headingOne">
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -509,7 +594,7 @@
|
||||||
if (properties.address) popupContent += `${properties.address}<br>`;
|
if (properties.address) popupContent += `${properties.address}<br>`;
|
||||||
if (properties.main_tag) popupContent += `<em>${properties.main_tag}</em><br>`;
|
if (properties.main_tag) popupContent += `<em>${properties.main_tag}</em><br>`;
|
||||||
if (properties.note) popupContent += `<small>Note: ${properties.note}</small><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) {
|
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
||||||
|
@ -539,11 +624,26 @@
|
||||||
const openInJOSMButton = document.getElementById('openInJOSM');
|
const openInJOSMButton = document.getElementById('openInJOSM');
|
||||||
if (openInJOSMButton) {
|
if (openInJOSMButton) {
|
||||||
openInJOSMButton.addEventListener('click', () => {
|
openInJOSMButton.addEventListener('click', () => {
|
||||||
const osmElements = geojsonData.features.map(f => ({
|
const place_nodes = [];
|
||||||
osm_id: f.properties.id.split('/')[1],
|
const place_ways = [];
|
||||||
osm_type: f.properties.id.split('/')[0]
|
const place_relations = [];
|
||||||
}));
|
const places = {{ geojson|raw }}.features;
|
||||||
openInJOSM(map, map_is_loaded, osmElements);
|
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, {
|
new Chart(completionCtx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
labels: completionLabels,
|
labels: completionLabels,
|
||||||
|
tension: 0.3,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Distribution du Taux de Complétion',
|
label: 'Distribution du Taux de Complétion',
|
||||||
data: completionValues,
|
data: completionValues,
|
||||||
|
@ -734,4 +836,49 @@ if(dc ){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -47,6 +47,12 @@
|
||||||
Podium des contributeurs OSM
|
Podium des contributeurs OSM
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue