mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-04 17:04:53 +02:00
carte thèmatique
This commit is contained in:
parent
2fd0d8d933
commit
5fe2804a1a
5 changed files with 253 additions and 142 deletions
|
@ -381,7 +381,7 @@ final class AdminController extends AbstractController
|
|||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
|
||||
// Normalisation des scores pondérés entre 0 et 100
|
||||
foreach ($podium_local as &$row) {
|
||||
if ($maxPondere > 0 && $row['completion_pondere'] !== null) {
|
||||
|
@ -391,7 +391,7 @@ final class AdminController extends AbstractController
|
|||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
|
||||
// Tri décroissant sur le score normalisé
|
||||
usort($podium_local, function ($a, $b) {
|
||||
return ($b['completion_pondere_normalisee'] ?? 0) <=> ($a['completion_pondere_normalisee'] ?? 0);
|
||||
|
@ -452,6 +452,8 @@ final class AdminController extends AbstractController
|
|||
$progression7Days[$type] = \App\Service\FollowUpService::calculate7DayProgression($stats, $type);
|
||||
}
|
||||
$progression7Days['places'] = \App\Service\FollowUpService::calculate7DayProgression($stats, 'places');
|
||||
|
||||
|
||||
|
||||
return $this->render('admin/stats.html.twig', [
|
||||
'stats' => $stats,
|
||||
|
@ -467,7 +469,7 @@ final class AdminController extends AbstractController
|
|||
'latestFollowups' => $latestFollowups,
|
||||
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
|
||||
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
|
||||
'progression7Days' => $progression7Days,
|
||||
'progression7Days' => $progression7Days,
|
||||
'all_types' => \App\Service\FollowUpService::getFollowUpThemes(),
|
||||
]);
|
||||
}
|
||||
|
@ -491,7 +493,7 @@ final class AdminController extends AbstractController
|
|||
$followups = $stats->getCityFollowUps();
|
||||
$countData = [];
|
||||
$completionData = [];
|
||||
|
||||
|
||||
foreach ($followups as $fu) {
|
||||
if ($fu->getName() === $theme . '_count') {
|
||||
$countData[] = [
|
||||
|
@ -525,7 +527,7 @@ final class AdminController extends AbstractController
|
|||
$josm_url = null;
|
||||
}
|
||||
// Fonction utilitaire pour extraire clé/valeur de la requête Overpass
|
||||
$extractTag = function($query) {
|
||||
$extractTag = function ($query) {
|
||||
if (preg_match('/\\[([a-zA-Z0-9:_-]+)\\]="([^"]+)"/', $query, $matches)) {
|
||||
return [$matches[1], $matches[2]];
|
||||
}
|
||||
|
@ -569,7 +571,7 @@ final class AdminController extends AbstractController
|
|||
'lat' => $place->getLat(),
|
||||
'lon' => $place->getLon(),
|
||||
'name' => $place->getName(),
|
||||
'tags' => [ 'main_tag' => $place->getMainTag() ],
|
||||
'tags' => ['main_tag' => $place->getMainTag()],
|
||||
'is_complete' => !empty($place->getName()),
|
||||
'osm_url' => 'https://www.openstreetmap.org/' . $place->getOsmKind() . '/' . $place->getOsmId(),
|
||||
];
|
||||
|
@ -577,7 +579,7 @@ final class AdminController extends AbstractController
|
|||
}
|
||||
$geojson = [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => array_map(function($obj) {
|
||||
'features' => array_map(function ($obj) {
|
||||
return [
|
||||
'type' => 'Feature',
|
||||
'geometry' => [
|
||||
|
@ -608,6 +610,7 @@ final class AdminController extends AbstractController
|
|||
'completion_data' => json_encode($completionData),
|
||||
'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
|
||||
'geojson' => json_encode($geojson),
|
||||
'overpass_query' => $overpass_query,
|
||||
'josm_url' => $josm_url,
|
||||
'center' => $center,
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
|
||||
|
@ -617,14 +620,16 @@ final class AdminController extends AbstractController
|
|||
#[Route('/admin/placeType/{osm_kind}/{osm_id}', name: 'app_admin_by_osm_id')]
|
||||
public function placeType(string $osm_kind, string $osm_id): Response
|
||||
{
|
||||
|
||||
|
||||
$place = $this->entityManager->getRepository(Place::class)->findOneBy(['osm_kind' => $osm_kind, 'osmId' => $osm_id]);
|
||||
if ($place) {
|
||||
$this->actionLogger->log('admin/placeType', ['osm_kind' => $osm_kind, 'osm_id' => $osm_id,
|
||||
'name' => $place->getName(),
|
||||
'code_insee' => $place->getZipCode(),
|
||||
'uuid' => $place->getUuidForUrl()
|
||||
]);
|
||||
$this->actionLogger->log('admin/placeType', [
|
||||
'osm_kind' => $osm_kind,
|
||||
'osm_id' => $osm_id,
|
||||
'name' => $place->getName(),
|
||||
'code_insee' => $place->getZipCode(),
|
||||
'uuid' => $place->getUuidForUrl()
|
||||
]);
|
||||
return $this->redirectToRoute('app_admin_commerce', ['id' => $place->getId()]);
|
||||
} else {
|
||||
$this->actionLogger->log('ERROR_admin/placeType', ['osm_kind' => $osm_kind, 'osm_id' => $osm_id]);
|
||||
|
@ -1520,11 +1525,11 @@ final class AdminController extends AbstractController
|
|||
public function podiumContributeursOsm(): Response
|
||||
{
|
||||
$this->actionLogger->log('admin/podium_contributeurs_osm', []);
|
||||
|
||||
|
||||
// Récupérer tous les lieux avec un utilisateur OSM
|
||||
$places = $this->entityManager->getRepository(Place::class)->findBy(['osm_user' => null], ['osm_user' => 'ASC']);
|
||||
$places = array_filter($places, fn($place) => $place->getOsmUser() !== null);
|
||||
|
||||
|
||||
// Compter les contributions par utilisateur
|
||||
$contributions = [];
|
||||
foreach ($places as $place) {
|
||||
|
@ -1536,13 +1541,13 @@ final class AdminController extends AbstractController
|
|||
$contributions[$user]++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Trier par nombre de contributions décroissant
|
||||
arsort($contributions);
|
||||
|
||||
|
||||
// Prendre les 10 premiers
|
||||
$topContributors = array_slice($contributions, 0, 10, true);
|
||||
|
||||
|
||||
return $this->render('admin/podium_contributeurs_osm.html.twig', [
|
||||
'contributors' => $topContributors
|
||||
]);
|
||||
|
@ -1555,15 +1560,17 @@ final class AdminController extends AbstractController
|
|||
|
||||
if ($request->isMethod('POST')) {
|
||||
$uploadedFile = $request->files->get('json_file');
|
||||
|
||||
|
||||
if (!$uploadedFile) {
|
||||
$this->addFlash('error', 'Aucun fichier JSON n\'a été fourni.');
|
||||
return $this->redirectToRoute('app_admin_import_stats');
|
||||
}
|
||||
|
||||
// Vérifier le type de fichier
|
||||
if ($uploadedFile->getClientMimeType() !== 'application/json' &&
|
||||
$uploadedFile->getClientOriginalExtension() !== 'json') {
|
||||
if (
|
||||
$uploadedFile->getClientMimeType() !== 'application/json' &&
|
||||
$uploadedFile->getClientOriginalExtension() !== 'json'
|
||||
) {
|
||||
$this->addFlash('error', 'Le fichier doit être au format JSON.');
|
||||
return $this->redirectToRoute('app_admin_import_stats');
|
||||
}
|
||||
|
@ -1598,7 +1605,7 @@ final class AdminController extends AbstractController
|
|||
|
||||
// Vérifier si l'objet Stats existe déjà
|
||||
$existingStats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zone]);
|
||||
|
||||
|
||||
if ($existingStats) {
|
||||
$skippedCount++;
|
||||
continue; // Ignorer les objets existants
|
||||
|
@ -1607,9 +1614,9 @@ final class AdminController extends AbstractController
|
|||
// Créer un nouvel objet Stats
|
||||
$stats = new Stats();
|
||||
$stats->setZone($zone)
|
||||
->setName($name)
|
||||
->setDateCreated(new \DateTime())
|
||||
->setDateModified(new \DateTime());
|
||||
->setName($name)
|
||||
->setDateCreated(new \DateTime())
|
||||
->setDateModified(new \DateTime());
|
||||
|
||||
// Remplir les champs optionnels
|
||||
if (isset($statData['population'])) {
|
||||
|
@ -1656,7 +1663,6 @@ final class AdminController extends AbstractController
|
|||
|
||||
$this->entityManager->persist($stats);
|
||||
$createdCount++;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "Ligne " . ($index + 1) . ": " . $e->getMessage();
|
||||
}
|
||||
|
@ -1680,7 +1686,6 @@ final class AdminController extends AbstractController
|
|||
'skipped' => $skippedCount,
|
||||
'errors' => count($errors)
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Erreur lors de l\'import : ' . $e->getMessage());
|
||||
$this->actionLogger->log('admin/import_stats_error', ['error' => $e->getMessage()]);
|
||||
|
@ -1696,7 +1701,7 @@ final class AdminController extends AbstractController
|
|||
public function exportOverpassCsv($insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
|
||||
}
|
||||
|
@ -1747,7 +1752,7 @@ final class AdminController extends AbstractController
|
|||
public function exportTableCsv($insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
|
||||
}
|
||||
|
@ -1784,7 +1789,7 @@ final class AdminController extends AbstractController
|
|||
$osmKind = $place->getOsmKind();
|
||||
$osmId = $place->getOsmId();
|
||||
$osmLink = ($osmKind && $osmId) ? 'https://www.openstreetmap.org/' . $osmKind . '/' . $osmId : '';
|
||||
|
||||
|
||||
// Construire l'adresse complète
|
||||
$address = '';
|
||||
if ($place->getHousenumber() && $place->getStreet()) {
|
||||
|
@ -1792,7 +1797,7 @@ final class AdminController extends AbstractController
|
|||
} elseif ($place->getStreet()) {
|
||||
$address = $place->getStreet();
|
||||
}
|
||||
|
||||
|
||||
fputcsv($output, [
|
||||
$place->getName() ?: '(sans nom)',
|
||||
$place->getEmail() ?: '',
|
||||
|
|
|
@ -480,7 +480,7 @@ class FollowUpService
|
|||
|
||||
public static function getFollowUpOverpassQueries(): array
|
||||
{
|
||||
return [
|
||||
return [
|
||||
'fire_hydrant' => 'nwr["emergency"="fire_hydrant"](area.searchArea);',
|
||||
'charging_station' => 'nwr["amenity"="charging_station"](area.searchArea);',
|
||||
'toilets' => 'nwr["amenity"="toilets"](area.searchArea);',
|
||||
|
@ -495,7 +495,7 @@ class FollowUpService
|
|||
'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);',
|
||||
'bicycle_parking' => 'nwr["amenity"="bicycle_parking"](area.searchArea);',
|
||||
'advertising_board' => 'nwr["advertising"="board"]["message"="political"](area.searchArea);',
|
||||
'building' => 'way["building"](area.searchArea);',
|
||||
'building' => 'nwr["building"](area.searchArea);',
|
||||
'email' => 'nwr["email"](area.searchArea);nwr["contact:email"](area.searchArea);',
|
||||
'bench' => 'nwr["amenity"="bench"](area.searchArea);',
|
||||
'waste_basket' => 'nwr["amenity"="waste_basket"](area.searchArea);',
|
||||
|
|
|
@ -619,4 +619,59 @@ QUERY;
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la progression sur 7j, 30j, 6 mois, 1 an pour une série de points [{date, value}]
|
||||
* @param array $data
|
||||
* @param \DateTime|null $now
|
||||
* @return array
|
||||
*/
|
||||
public function calculateProgressions(array $data, ?\DateTime $now = null): array
|
||||
{
|
||||
$periods = [
|
||||
'7j' => '-7 days',
|
||||
'30j' => '-30 days',
|
||||
'6m' => '-6 months',
|
||||
'1a' => '-1 year',
|
||||
];
|
||||
$progressions = [];
|
||||
if ($now === null) $now = new \DateTime();
|
||||
$calculateDelta = function(array $data, \DateTime $refDate) {
|
||||
if (empty($data)) return null;
|
||||
$last = end($data)['value'];
|
||||
$exactRef = null;
|
||||
foreach (array_reverse($data) as $point) {
|
||||
$pointDate = new \DateTime($point['date']);
|
||||
if ($pointDate <= $refDate) {
|
||||
$exactRef = $point['value'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($exactRef !== null) return $last - $exactRef;
|
||||
$beforeRef = null; $afterRef = null; $beforeDate = null; $afterDate = null;
|
||||
foreach (array_reverse($data) as $point) {
|
||||
$pointDate = new \DateTime($point['date']);
|
||||
if ($pointDate < $refDate) { $beforeRef = $point['value']; $beforeDate = $pointDate; break; }
|
||||
}
|
||||
foreach ($data as $point) {
|
||||
$pointDate = new \DateTime($point['date']);
|
||||
if ($pointDate > $refDate) { $afterRef = $point['value']; $afterDate = $pointDate; break; }
|
||||
}
|
||||
if ($beforeRef !== null && $afterRef !== null && $beforeDate && $afterDate) {
|
||||
$timeDiff = $afterDate->getTimestamp() - $beforeDate->getTimestamp();
|
||||
$refTimeDiff = $refDate->getTimestamp() - $beforeDate->getTimestamp();
|
||||
$ratio = $refTimeDiff / $timeDiff;
|
||||
$interpolatedRef = $beforeRef + ($afterRef - $beforeRef) * $ratio;
|
||||
return $last - $interpolatedRef;
|
||||
}
|
||||
if ($beforeRef !== null) return $last - $beforeRef;
|
||||
if ($afterRef !== null) return $last - $afterRef;
|
||||
return null;
|
||||
};
|
||||
foreach ($periods as $label => $mod) {
|
||||
$refDate = (clone $now)->modify($mod);
|
||||
$progressions[$label] = $calculateDelta($data, $refDate);
|
||||
}
|
||||
return $progressions;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
<a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': theme}) }}" class="btn btn-primary me-2">
|
||||
<i class="bi bi-graph-up"></i> Graphe détaillé
|
||||
</a>
|
||||
<a href="https://osm-mon-commerce.fr/?insee={{ stats.zone }}" target="_blank" class="btn btn-success me-2">
|
||||
<a href="{{ path('app_public_index') }}" target="_blank" class="btn btn-success me-2">
|
||||
<i class="bi bi-globe"></i> OSM Mon Commerce
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -172,6 +172,14 @@
|
|||
{% endif %}
|
||||
<div id="themeMap"></div>
|
||||
|
||||
{% if overpass_query is defined %}
|
||||
<div class="mb-3">
|
||||
<a href="https://overpass-turbo.eu/?Q={{ overpass_query|url_encode }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Voir la requête sur Overpass Turbo
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="currentCount">-</div>
|
||||
|
@ -191,21 +199,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-tabs">
|
||||
<button class="chart-tab active" data-chart="count">Nombre d'objets</button>
|
||||
<button class="chart-tab" data-chart="completion">Pourcentage de complétion</button>
|
||||
</div>
|
||||
|
||||
<div class="chart-content active" id="count-chart">
|
||||
<div class="chart-container">
|
||||
<canvas id="countChart"></canvas>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
{# <div class="card p-3">
|
||||
<h5>Progression</h5>
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>Période</th><th>Nombre</th><th>Complétion (%)</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>7 jours</td><td>{{ progressions.count['7j'] is not null ? '%+d'|format(progressions.count['7j']) : '–' }}</td><td>{{ progressions.completion['7j'] is not null ? '%+.1f'|format(progressions.completion['7j']) : '–' }}</td></tr>
|
||||
<tr><td>30 jours</td><td>{{ progressions.count['30j'] is not null ? '%+d'|format(progressions.count['30j']) : '–' }}</td><td>{{ progressions.completion['30j'] is not null ? '%+.1f'|format(progressions.completion['30j']) : '–' }}</td></tr>
|
||||
<tr><td>6 mois</td><td>{{ progressions.count['6m'] is not null ? '%+d'|format(progressions.count['6m']) : '–' }}</td><td>{{ progressions.completion['6m'] is not null ? '%+.1f'|format(progressions.completion['6m']) : '–' }}</td></tr>
|
||||
<tr><td>1 an</td><td>{{ progressions.count['1a'] is not null ? '%+d'|format(progressions.count['1a']) : '–' }}</td><td>{{ progressions.completion['1a'] is not null ? '%+.1f'|format(progressions.completion['1a']) : '–' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div> #}
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<div class="ms-auto">
|
||||
<label for="basemapSelect" class="form-label mb-1">Fond de carte :</label>
|
||||
<select id="basemapSelect" class="form-select">
|
||||
<option value="streets">MapTiler Streets</option>
|
||||
<option value="satellite">BD Ortho IGN</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-content" id="completion-chart">
|
||||
<div class="chart-container">
|
||||
<canvas id="completionChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="themeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -224,34 +245,78 @@
|
|||
console.log('[DEBUG] mapCenter:', mapCenter);
|
||||
if (mapToken && geojson && geojson.features && geojson.features.length > 0) {
|
||||
console.log('[DEBUG] Initialisation de la carte Maplibre...');
|
||||
const map = new maplibregl.Map({
|
||||
container: 'themeMap',
|
||||
style: `https://api.maptiler.com/maps/streets/style.json?key=${mapToken}`,
|
||||
center: mapCenter || geojson.features[0].geometry.coordinates,
|
||||
zoom: 13
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl());
|
||||
geojson.features.forEach(f => {
|
||||
let color = f.properties.is_complete ? '#198754' : '#adb5bd';
|
||||
if (!f.properties.is_complete && (f.properties.tags && (f.properties.tags.name || f.properties.tags.operator))) {
|
||||
color = '#ffc107'; // partiel
|
||||
let mapInstance = null;
|
||||
function getStyleUrl(style) {
|
||||
if (style === 'streets') {
|
||||
return `https://api.maptiler.com/maps/streets/style.json?key=${mapToken}`;
|
||||
} else if (style === 'satellite') {
|
||||
// BD Ortho IGN WMTS (clé publique, usage limité)
|
||||
return {
|
||||
version: 8,
|
||||
sources: {
|
||||
"bdortho": {
|
||||
"type": "raster",
|
||||
"tiles": [
|
||||
"https://wxs.ign.fr/essentiels/geoportail/wmts?layer=ORTHOIMAGERY.ORTHOPHOTOS&style=normal&tilematrixset=PM&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/jpeg&TileMatrix={z}&TileCol={x}&TileRow={y}"
|
||||
],
|
||||
"tileSize": 256,
|
||||
"attribution": "Données © IGN BD Ortho"
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
"id": "bdortho",
|
||||
"type": "raster",
|
||||
"source": "bdortho",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 19
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
const marker = new maplibregl.Marker({ color: color })
|
||||
.setLngLat(f.geometry.coordinates)
|
||||
.setPopup(new maplibregl.Popup({ offset: 18 })
|
||||
.setHTML(`
|
||||
<div style='min-width:180px'>
|
||||
<strong>${f.properties.name || '(sans nom)'}</strong><br>
|
||||
<span class='text-muted'>${f.properties.osm_kind} ${f.properties.id}</span><br>
|
||||
<span style='font-size:0.95em;'>
|
||||
${Object.entries(f.properties.tags).map(([k,v]) => `<span><b>${k}</b>: ${v}</span>`).join('<br>')}
|
||||
</span>
|
||||
<a href='${f.properties.osm_url}' target='_blank'>Voir sur OSM</a>
|
||||
</div>
|
||||
`)
|
||||
)
|
||||
.addTo(map);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
function initMap(style) {
|
||||
if (mapInstance) { mapInstance.remove(); }
|
||||
const styleUrl = getStyleUrl(style);
|
||||
mapInstance = new maplibregl.Map({
|
||||
container: 'themeMap',
|
||||
style: styleUrl,
|
||||
center: mapCenter || (geojson.features && geojson.features[0] ? geojson.features[0].geometry.coordinates : [2,48]),
|
||||
zoom: 13
|
||||
});
|
||||
mapInstance.addControl(new maplibregl.NavigationControl());
|
||||
geojson.features.forEach(f => {
|
||||
let color = f.properties.is_complete ? '#198754' : '#adb5bd';
|
||||
if (!f.properties.is_complete && (f.properties.tags && (f.properties.tags.name || f.properties.tags.operator))) {
|
||||
color = '#ffc107';
|
||||
}
|
||||
const marker = new maplibregl.Marker({ color: color })
|
||||
.setLngLat(f.geometry.coordinates)
|
||||
.setPopup(new maplibregl.Popup({ offset: 18 })
|
||||
.setHTML(`
|
||||
<div style='min-width:180px'>
|
||||
<strong>${f.properties.name || '(sans nom)'}</strong><br>
|
||||
<span class='text-muted'>${f.properties.osm_kind} ${f.properties.id}</span><br>
|
||||
<span style='font-size:0.95em;'>
|
||||
${Object.entries(f.properties.tags).map(([k,v]) => `<span><b>${k}</b>: ${v}</span>`).join('<br>')}
|
||||
</span>
|
||||
<a href='${f.properties.osm_url}' target='_blank'>Voir sur OSM</a>
|
||||
</div>
|
||||
`)
|
||||
)
|
||||
.addTo(mapInstance);
|
||||
});
|
||||
}
|
||||
// Initialisation par défaut
|
||||
initMap('streets');
|
||||
// Sélecteur de fond de carte
|
||||
const basemapSelect = document.getElementById('basemapSelect');
|
||||
if (basemapSelect) {
|
||||
basemapSelect.addEventListener('change', function() {
|
||||
initMap(this.value);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn('[DEBUG] Carte non initialisée : conditions non remplies.');
|
||||
if (!mapToken) {
|
||||
|
@ -335,88 +400,74 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Graphique du nombre d'objets
|
||||
const countCtx = document.getElementById('countChart').getContext('2d');
|
||||
const countChart = new Chart(countCtx, {
|
||||
// Graphique fusionné
|
||||
const ctx = document.getElementById('themeChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Nombre d\'objets',
|
||||
data: countData.map(d => ({
|
||||
x: new Date(d.date),
|
||||
y: d.value
|
||||
})),
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
scales: {
|
||||
...commonOptions.scales,
|
||||
y: {
|
||||
...commonOptions.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Nombre d\'objets'
|
||||
}
|
||||
datasets: [
|
||||
{
|
||||
label: "Nombre d'objets",
|
||||
data: countData.map(d => ({ x: new Date(d.date), y: d.value })),
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.1,
|
||||
yAxisID: 'y1',
|
||||
},
|
||||
{
|
||||
label: 'Pourcentage de complétion',
|
||||
data: completionData.map(d => ({ x: new Date(d.date), y: d.value })),
|
||||
borderColor: '#198754',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.1,
|
||||
yAxisID: 'y2',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Graphique de la complétion
|
||||
const completionCtx = document.getElementById('completionChart').getContext('2d');
|
||||
const completionChart = new Chart(completionCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Pourcentage de complétion',
|
||||
data: completionData.map(d => ({
|
||||
x: new Date(d.date),
|
||||
y: d.value
|
||||
})),
|
||||
borderColor: '#198754',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.1
|
||||
}]
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
tooltip: { mode: 'index', intersect: false }
|
||||
},
|
||||
interaction: { mode: 'nearest', axis: 'x', intersect: false },
|
||||
scales: {
|
||||
...commonOptions.scales,
|
||||
y: {
|
||||
...commonOptions.scales.y,
|
||||
x: {
|
||||
type: 'time',
|
||||
time: { unit: 'day', displayFormats: { day: 'dd/MM/yyyy' } },
|
||||
title: { display: true, text: 'Date' }
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: { display: true, text: "Nombre d'objets" },
|
||||
beginAtZero: true
|
||||
},
|
||||
y2: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
title: { display: true, text: 'Complétion (%)' },
|
||||
min: 0,
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Pourcentage de complétion (%)'
|
||||
}
|
||||
grid: { drawOnChartArea: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion des onglets
|
||||
document.querySelectorAll('.chart-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
// Retirer la classe active de tous les onglets
|
||||
document.querySelectorAll('.chart-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.chart-content').forEach(c => c.classList.remove('active'));
|
||||
|
||||
// Ajouter la classe active à l'onglet cliqué
|
||||
this.classList.add('active');
|
||||
const chartId = this.getAttribute('data-chart') + '-chart';
|
||||
document.getElementById(chartId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Initialiser les statistiques
|
||||
updateStats();
|
||||
|
||||
// Ajout debug JS requête Overpass
|
||||
{% if overpass_query is defined %}
|
||||
console.log('[DEBUG][Overpass] Requête envoyée à Overpass :', `{{ overpass_query|e('js') }}`);
|
||||
{% else %}
|
||||
console.log('[DEBUG][Overpass] Aucune requête Overpass transmise à la page.');
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue