2025-05-26 23:51:46 +02:00
{% extends 'base.html.twig' %}
2025-06-04 00:16:56 +02:00
{% block title %} {{ 'display.stats' | trans }} - {{ stats .zone }}
{{ stats .name }} {% endblock %}
2025-05-26 23:51:46 +02:00
2025-06-01 19:52:56 +02:00
{% block stylesheets %}
{{ parent ( ) }}
2025-06-03 14:58:23 +02:00
<link href=' {{ asset ( 'js/maplibre/maplibre-gl.css' ) }} ' rel='stylesheet' />
2025-06-06 11:50:37 +02:00
<style>
.completion-circle {
fill-opacity: 0.6;
stroke: #fff;
stroke-width: 3;
}
2025-06-06 12:17:03 +02:00
#distribution_completion {
height: 300px;
margin: 20px 0;
}
2025-06-17 13:23:47 +02:00
.completion-info {
margin-bottom: 2rem;
}
2025-06-19 12:49:30 +02:00
.osm-modification-info {
font-size: 0.85rem;
line-height: 1.3;
}
.osm-modification-info .text-muted {
font-size: 0.75rem;
}
.osm-modification-info a {
text-decoration: none;
color: #0d6efd;
}
.osm-modification-info a:hover {
text-decoration: underline;
}
.osm-freshness-info {
font-size: 0.95rem;
line-height: 1.4;
}
.osm-freshness-info .alert {
border-left: 4px solid #0dcaf0;
background-color: #f8f9fa;
}
2025-06-29 18:32:24 +02:00
.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;
}
2025-07-05 14:31:50 +02:00
.compact-theme-card {
transition: transform 0.2s ease-in-out;
}
.compact-theme-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
}
.theme-title a {
font-size: 0.85rem;
line-height: 1.2;
}
.theme-stats {
font-size: 0.75rem;
color: #6c757d;
}
.theme-actions .btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.theme-row-scroll {
overflow-x: auto;
white-space: nowrap;
padding-bottom: 0.5rem;
}
.theme-row-scroll .col-auto {
display: inline-block;
float: none;
}
.theme-row-scroll .compact-theme-card {
display: inline-block;
vertical-align: top;
margin-right: 8px;
}
.tab-content {
margin-top: 1.5rem;
}
.table-theme th, .table-theme td {
vertical-align: middle;
font-size: 0.95em;
}
.table-theme th {
background: #f8f9fa;
}
2025-06-06 11:50:37 +02:00
</style>
2025-06-01 19:52:56 +02:00
{% endblock %}
2025-06-17 18:27:19 +02:00
2025-05-26 23:51:46 +02:00
{% block body %}
<div class="container">
2025-05-29 16:50:25 +02:00
<div class="mt-4 p-4">
2025-06-04 00:16:56 +02:00
<div class="row">
<div class="col-md-6 col-12">
<h1 class="title"> {{ 'display.stats' | trans }} - {{ stats .zone }}
{{ stats .name }} - {{ stats .completionPercent }} % complété</h1>
</div>
<div class="col-md-6 col-12">
2025-06-24 13:16:48 +02:00
<a href=" {{ path ( 'app_admin_labourer' , { 'insee_code' : stats .zone , 'deleteMissing' : 1 } ) }} " class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
2025-07-05 10:29:53 +02:00
<a href=" {{ path ( 'app_admin_labourer' , { 'insee_code' : stats .zone , 'deleteMissing' : 1 , 'disableFollowUpCleanup' : 1 } ) }} " class="btn btn-warning ms-2" id="labourer-no-cleanup" title="Labourer sans nettoyer les suivis OSM">
<i class="bi bi-shield-check"></i> Labourer (sans nettoyage)
</a>
2025-06-29 16:41:18 +02:00
<a href=" {{ path ( 'admin_followup_graph' , { 'insee_code' : stats .zone } ) }} " class="btn btn-info ms-2" id="followup-graph-link">
<i class="bi bi-graph-up"></i> Suivi OSM (graphes)
</a>
2025-06-06 11:50:37 +02:00
<button id="openInJOSM" class="btn btn-secondary ms-2">
<i class="bi bi-map"></i> Ouvrir dans JOSM
</button>
2025-07-12 12:03:40 +02:00
<a href=" {{ path ( 'app_public_stats_evolutions' , { 'insee_code' : stats .zone } ) }} " class="btn btn-outline-info ms-2">
<i class="bi bi-activity"></i> Évolutions des objets
</a>
2025-07-12 12:53:06 +02:00
<a href=" {{ path ( 'admin_street_completion' , { 'insee_code' : stats .zone } ) }} " class="btn btn-outline-success ms-2">
<i class="bi bi-signpost"></i> Complétion des rues
</a>
2025-07-14 19:27:07 +02:00
<a href=" {{ path ( 'admin_speed_limit' , { 'insee_code' : stats .zone } ) }} " class="btn btn-outline-danger ms-2">
<i class="bi bi-speedometer2"></i> Limites de vitesse
</a>
2025-06-04 00:16:56 +02:00
</div>
</div>
2025-06-17 16:23:29 +02:00
{% if stats .population %}
<div class="row mb-3">
<div class="col-md-4 col-12">
<span class="badge bg-info">
<i class="bi bi-people"></i> Population : {{ stats .population | number_format ( 0 , '.' , ' ' ) }}
</span>
</div>
<div class="col-md-4 col-12">
<span class="badge bg-secondary">
<i class="bi bi-shop"></i> 1 lieu pour
{% set ratio = ( stats .population and stats .places | length > 0 ) ? ( stats .population / stats .places | length ) | round ( 0 , 'ceil' ) : '?' %}
{{ ratio | number_format ( 0 , '.' , ' ' ) }} habitants
</span>
</div>
<div class="col-md-4 col-12">
<span class="badge bg-success">
2025-06-17 19:38:44 +02:00
<i class="bi bi-pencil-square"></i> {{ stats .getAvecNote ( ) }} / {{ stats .places | length }} lieux avec note
2025-06-17 16:23:29 +02:00
</span>
</div>
</div>
2025-06-24 12:30:39 +02:00
{% if stats .budgetAnnuel %}
<div class="row mb-3">
<div class="col-md-4 col-12">
<span class="badge bg-warning text-dark">
<i class="bi bi-cash-coin"></i> Budget annuel : {{ stats .budgetAnnuel | number_format ( 0 , '.' , ' ' ) }} €
</span>
</div>
<div class="col-md-4 col-12">
<span class="badge bg-warning text-dark">
<i class="bi bi-cash-stack"></i> Budget par habitant :
{% if stats .population > 0 %}
{{ ( stats .budgetAnnuel / stats .population ) | number_format ( 0 , '.' , ' ' ) }} €
{% else %}
?
{% endif %}
</span>
</div>
</div>
{% endif %}
2025-06-17 16:23:29 +02:00
{% endif %}
2025-06-19 12:49:30 +02:00
2025-06-26 23:14:22 +02:00
2025-06-19 12:49:30 +02:00
2025-06-03 12:51:20 +02:00
<div class="row">
2025-06-29 18:32:24 +02:00
<div id="followups">
{% set overpass_type_queries = {
'fire_hydrant': 'nwr["emergency"="fire_hydrant"](area.searchArea);',
'charging_station': 'nwr["amenity"="charging_station"](area.searchArea);',
2025-07-13 16:59:45 +02:00
'bicycle_parking' : 'nwr["amenity"="bicycle_parking"](area.searchArea);',
2025-06-29 18:32:24 +02:00
'toilets': 'nwr["amenity"="toilets"](area.searchArea);',
2025-07-14 13:13:52 +02:00
'bus_stop': 'nwr["highway"="bus_stop"](area.searchArea);nwr["public_transport"="platform"](area.searchArea);',
2025-06-29 18:32:24 +02:00
'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);',
2025-07-13 16:59:45 +02:00
'drinking_water' : 'nwr["amenity"="drinking_water"](area.searchArea);',
2025-06-29 18:32:24 +02:00
'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);'
} %}
2025-07-05 14:31:50 +02:00
{% set theme_groups = {
'emergency': ['fire_hydrant', 'defibrillator'],
2025-07-13 16:59:45 +02:00
'transport': ['bus_stop', 'charging_station', 'bicycle_parking'],
'healthcare': ['healthcare', 'laboratory', 'drinking_water'],
2025-07-05 14:31:50 +02:00
'education': ['school'],
'security': ['police', 'camera'],
'infrastructure': ['toilets', 'recycling', 'substation']
} %}
<div class="row mb-4">
<div class="col-12">
<ul class="nav nav-tabs" id="themeTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-table" data-bs-toggle="tab" data-bs-target="#tabTableContent" type="button" role="tab" aria-controls="tabTableContent" aria-selected="true">Tableau</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-cards" data-bs-toggle="tab" data-bs-target="#tabCardsContent" type="button" role="tab" aria-controls="tabCardsContent" aria-selected="false">Cartes</button>
</li>
</ul>
<div class="tab-content" id="themeTabsContent">
<div class="tab-pane fade show active" id="tabTableContent" role="tabpanel" aria-labelledby="tab-table">
2025-07-05 15:25:33 +02:00
<table class="table table-theme">
2025-07-05 14:31:50 +02:00
<thead>
<tr>
2025-07-05 16:53:12 +02:00
<th>Catégorie</th>
2025-07-05 14:31:50 +02:00
<th>Thème</th>
<th>Nombre</th>
2025-07-05 16:53:12 +02:00
<th>Complétion</th>
<th>Évolution</th>
<th>Graphique</th>
2025-07-05 14:31:50 +02:00
</tr>
</thead>
<tbody>
{% for group_name , group_types in theme_groups %}
{% for type in group_types %}
{% set data = latestFollowups [ type ] | default ( null ) %}
{% set completion = data and data .completion is defined ? data .completion .getMeasure ( ) : null %}
{% set count = data and data .count is defined ? data .count .getMeasure ( ) : null %}
{% set completion_class = '' %}
{% if completion is not null %}
{% if completion < 4 0 %}
{% set completion_class = 'completion-low' %}
{% elseif completion < 8 0 %}
{% set completion_class = 'completion-medium' %}
{% else %}
{% set completion_class = 'completion-high' %}
2025-07-05 10:29:53 +02:00
{% endif %}
2025-07-05 14:31:50 +02:00
{% endif %}
<tr>
<td class="text-muted">
{% if loop .first %}
{% if group_name == 'emergency' %} 🚨 Urgence
{% elseif group_name == 'transport' %} 🚌 Transport
{% elseif group_name == 'healthcare' %} 🏥 Santé
{% elseif group_name == 'education' %} 🎓 Éducation
{% elseif group_name == 'security' %} 🛡️ Sécurité
{% elseif group_name == 'infrastructure' %} 🏗️ Infrastructure
{% else %} {{ group_name | capitalize }}
{% endif %}
{% endif %}
</td>
<td>
<i class="bi {{ followup_icons [ type ] | default ( 'bi-question-circle' ) }} "></i>
{{ followup_labels [ type ] | default ( type | capitalize ) }}
</td>
<td> {{ count is not null ? count : '?' }} </td>
<td><span class="completion-badge {{ completion_class }} "></span> {{ completion is not null ? completion ~ '%' : '?' }} </td>
<td>
{% if progression7Days [ type ] is defined %}
{% set countDelta = progression7Days [ type ] .count %}
{% set completionDelta = progression7Days [ type ] .completion %}
<span class="text-muted small">
{% if countDelta is not null %}
<span title="Progression sur 7 jours - Nombre d'objets">
{{ countDelta > 0 ? '+' ~ countDelta : countDelta == 0 ? '0' : countDelta }}
</span>
{% endif %}
{% if completionDelta is not null %}
<span title="Progression sur 7 jours - Complétion">
{{ completionDelta > 0 ? '+' ~ completionDelta | round ( 1 ) : completionDelta == 0 ? '0' : completionDelta | round ( 1 ) }} %
</span>
{% endif %}
</span>
{% endif %}
</td>
<td>
<a href=" {{ path ( 'admin_followup_theme_graph' , { 'insee_code' : stats .zone , 'theme' : type } ) }} " target="_blank" class="btn btn-sm btn-outline-primary" title="Voir le graphique">
<i class="bi bi-graph-up"></i>
</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
2025-06-29 18:32:24 +02:00
</div>
2025-07-05 14:31:50 +02:00
<div class="tab-pane fade" id="tabCardsContent" role="tabpanel" aria-labelledby="tab-cards">
2025-07-05 15:25:33 +02:00
{% set all_types = followup_labels | keys %}
<div class="row">
{% for type in all_types %}
{% set data = latestFollowups [ type ] | default ( null ) %}
{% 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 and data .completion is defined ? data .completion .getMeasure ( ) : null %}
{% set completion_class = '' %}
{% if completion is not null %}
{% if completion < 4 0 %}
{% set completion_class = 'completion-low' %}
{% elseif completion < 8 0 %}
{% set completion_class = 'completion-medium' %}
{% else %}
{% set completion_class = 'completion-high' %}
2025-07-05 14:31:50 +02:00
{% endif %}
2025-07-05 15:25:33 +02:00
{% endif %}
<div class="col-auto">
<div class="card shadow-sm text-center compact-theme-card" style="min-width: 120px; max-width: 140px;">
<div class="card-body p-2">
<div class="d-flex align-items-center justify-content-between mb-1">
<span class="completion-badge {{ completion_class }} "></span>
<i class="bi {{ followup_icons [ type ] | default ( 'bi-question-circle' ) }} fs-4"></i>
</div>
<div class="theme-title mb-1">
<a href="https://overpass-api.de/api/interpreter?data= {{ overpass_query | url_encode }} " target="_blank" class="fw-bold text-decoration-none text-dark small" title="Voir le JSON Overpass">
{{ followup_labels [ type ] | default ( type | capitalize ) }}
</a>
</div>
<div class="theme-stats small">
<span title="Nombre"> {{ data and data .count is defined ? data .count .getMeasure ( ) : '?' }} </span> |
<span title="Complétion"> {{ completion is not null ? completion : '?' }} %</span>
</div>
<div class="theme-actions mt-1">
<a href=" {{ path ( 'admin_followup_theme_graph' , { 'insee_code' : stats .zone , 'theme' : type } ) }} " target="_blank" class="btn btn-sm btn-outline-primary btn-sm" title="Voir le graphique">
<i class="bi bi-graph-up"></i>
</a>
<a href="http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data= {{ overpass_query | url_encode }} " target="_blank" class="btn btn-sm btn-outline-dark btn-sm ms-1" title="Ouvrir dans JOSM">
<i class="bi bi-box-arrow-up-right"></i> JOSM
</a>
2025-07-05 14:31:50 +02:00
</div>
</div>
2025-07-05 17:21:18 +02:00
</div>
</div>
{% endfor %}
</div>
<div class="row mb-4">
{% for type in all_types %}
<div class="col-md-4 col-12 mb-2">
<span class="fw-bold"> {{ followup_labels [ type ] | default ( type | capitalize ) }} </span>
<button class="btn btn-link p-0 ms-1" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-tags- {{ type }} " aria-expanded="false" aria-controls="collapse-tags- {{ type }} " title="Voir les critères de complétion">
<i class="bi bi-question-circle"></i>
</button>
<div class="collapse mt-2" id="collapse-tags- {{ type }} ">
<div class="card card-body p-2 small">
<span class="fw-bold">Critères de complétion attendus :</span>
<ul class="mb-0">
{% for tag in completion_tags [ type ] ? ? [ ] %}
<li><code> {{ tag }} </code></li>
{% else %}
<li><span class="text-muted">Aucun critère défini</span></li>
{% endfor %}
</ul>
</div>
2025-07-05 15:25:33 +02:00
</div>
2025-07-05 14:31:50 +02:00
</div>
2025-07-05 15:25:33 +02:00
{% endfor %}
</div>
2025-06-29 18:32:24 +02:00
</div>
</div>
2025-07-05 14:31:50 +02:00
</div>
2025-06-29 18:32:24 +02:00
</div>
</div>
2025-06-03 12:51:20 +02:00
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge {% if stats .getCompletionPercent ( ) > 8 5 %} bg-success {% else %} bg-warning {% endif %} ">
{{ stats .getCompletionPercent ( ) }} %
</span>
complété sur les critères donnés.
2025-06-03 12:51:20 +02:00
</div>
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge bg-primary">
<i class="bi bi-building"></i> {{ stats .places | length }}
2025-06-17 19:38:44 +02:00
</span>lieux dans la zone.
2025-06-03 12:51:20 +02:00
</div>
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge bg-primary">
<i class="bi bi-clock"></i> {{ stats .getAvecHoraires ( ) }}
</span>
2025-06-17 19:38:44 +02:00
lieux avec horaires.
2025-06-03 12:51:20 +02:00
</div>
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge bg-primary">
<i class="bi bi-map"></i> {{ stats .getAvecAdresse ( ) }}
</span>
2025-06-17 19:38:44 +02:00
lieux avec adresse.
2025-06-03 12:51:20 +02:00
</div>
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge bg-primary">
<i class="bi bi-globe"></i> {{ stats .getAvecSite ( ) }}
</span>
2025-06-17 19:38:44 +02:00
lieux avec site web renseigné.
2025-06-03 12:51:20 +02:00
</div>
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge bg-primary">
2025-06-03 12:51:20 +02:00
<i class="bi bi-arrow-up-right"></i>
2025-06-06 16:16:34 +02:00
{{ stats .getAvecAccessibilite ( ) }}
</span>
2025-06-17 19:38:44 +02:00
lieux avec accessibilité PMR renseignée.
2025-06-03 12:51:20 +02:00
</div>
<div class="col-md-3 col-12">
2025-06-06 16:16:34 +02:00
<span class="badge bg-primary">
<i class="bi bi-chat-dots"></i> {{ stats .getAvecNote ( ) }}
</span>
2025-06-17 19:38:44 +02:00
lieux avec note renseignée.
2025-06-03 12:51:20 +02:00
</div>
</div>
2025-06-17 13:23:47 +02:00
2025-06-17 18:27:19 +02:00
2025-06-03 12:51:20 +02:00
2025-06-04 00:16:56 +02:00
<div id="maploader">
<div class="spinner-border" role="status">
<i class="bi bi-load bi-spin"></i>
<span class="visually-hidden">Chargement de la carte...</span>
</div>
</div>
2025-06-09 00:11:16 +02:00
<div class="d-flex justify-content-end mb-2">
<div class="btn-group" role="group">
2025-06-09 00:34:33 +02:00
<button type="button" class="btn btn-outline-primary" id="circleMarkersBtn">
2025-06-09 00:11:16 +02:00
<i class="bi bi-circle"></i> Cercles
</button>
2025-06-09 00:34:33 +02:00
<button type="button" class="btn btn-outline-primary active" id="dropMarkersBtn">
2025-06-09 00:11:16 +02:00
<i class="bi bi-geo-alt"></i> Gouttes
</button>
</div>
2025-06-29 18:32:24 +02:00
<button id="btn-geolocate" class="btn btn-outline-primary btn-sm">
<i class="bi bi-geo-alt"></i> Me localiser
</button>
2025-06-09 00:11:16 +02:00
</div>
2025-06-18 01:23:16 +02:00
<div id="map" style="height: 400px; width: 100%; margin-bottom: 1rem;"></div>
2025-06-26 19:18:29 +02:00
<div class="row ">
<div class="col-md-6 col-12 ">
2025-07-05 12:37:01 +02:00
<canvas id="repartition_tags" width="600" height="400" style="max-width:100%; margin: 20px 0;"></canvas>
2025-06-26 19:18:29 +02:00
</div>
<div class="col-md-6 col-12 ">
2025-06-23 23:49:48 +02:00
<div class="card">
<div class="card-header">
<i class="bi bi-calendar-event"></i> Fréquence des mises à jour par trimestre pour {{ stats .name }}
</div>
<div class="card-body">
<canvas id="modificationsByQuarterChart" style="min-height: 250px; width: 100%;"></canvas>
</div>
</div>
2025-06-26 19:18:29 +02:00
</div>
</div>
2025-06-23 23:49:48 +02:00
2025-06-06 16:16:34 +02:00
<div id="attribution">
<a href="https://www.openstreetmap.org/copyright">Données OpenStreetMap</a>
</div>
2025-06-03 12:51:20 +02:00
</div>
2025-05-26 23:51:46 +02:00
<div class="card mt-4">
2025-06-26 23:14:22 +02:00
{% include 'admin/stats_history.html.twig' with { stat : stats } %}
2025-07-05 12:37:01 +02:00
<canvas id="distribution_completion" class="mt-4 mb-4" height="400"></canvas>
2025-06-06 12:17:03 +02:00
2025-06-17 19:38:44 +02:00
2025-06-04 00:16:56 +02:00
<div class="row">
<div class="col-md-6 col-12">
<h1 class="card-title p-4">Tableau des {{ stats .places | length }} lieux</h1>
</div>
<div class="col-md-6 col-12">
2025-07-05 10:59:37 +02:00
<div class="btn-group mt-4" role="group">
<a href=" {{ path ( 'app_admin_export_overpass_csv' , { 'insee_code' : stats .zone } ) }} " class="btn btn-primary">
<i class="bi bi-filetype-csv"></i>
Export Overpass CSV
</a>
<a href=" {{ path ( 'app_admin_export_table_csv' , { 'insee_code' : stats .zone } ) }} " class="btn btn-success">
<i class="bi bi-table"></i>
Export Tableau CSV
</a>
</div>
2025-06-04 00:16:56 +02:00
</div>
</div>
2025-06-29 10:29:27 +02:00
2025-06-29 10:22:24 +02:00
<div id="table_container" class="table-container" >
<table id="stats-table" class="table table-bordered table-striped table-hover table-responsive table-sort">
2025-06-03 11:37:27 +02:00
{% include 'admin/stats/table-head.html.twig' %}
2025-05-26 23:51:46 +02:00
<tbody>
{% for commerce in stats .places %}
2025-06-03 11:37:27 +02:00
{% include 'admin/stats/row.html.twig' %}
2025-05-26 23:51:46 +02:00
{% endfor %}
</tbody>
</table>
2025-06-29 10:22:24 +02:00
</div>
2025-05-26 23:51:46 +02:00
</div>
2025-06-03 12:51:20 +02:00
2025-06-29 18:32:24 +02:00
<div class="card mt-4" id="podium">
2025-06-27 11:14:27 +02:00
<div class="card-header">
<h2>Podium des contributeurs OSM de cette ville</h2>
</div>
<div class="card-body">
2025-06-29 10:22:24 +02:00
<div class="table-container" >
<table class="table table-striped table-bordered mt-4" style="max-width:800px">
<thead class="table-dark">
2025-06-27 11:14:27 +02:00
<tr>
2025-06-29 10:22:24 +02:00
<th scope="col">#</th>
<th scope="col">Utilisateur OSM</th>
<th scope="col">Nombre de lieux</th>
<th scope="col">Score de complétion moyen</th>
<th scope="col">Score de complétion pondéré</th>
<th scope="col">Score pondéré normalisé (0-100)</th>
2025-06-27 11:14:27 +02:00
</tr>
2025-06-29 10:22:24 +02:00
</thead>
<tbody>
{% for row in podium_local %}
<tr>
<th scope="row"> {{ loop .index }} </th>
<td>
2025-06-29 18:32:24 +02:00
<a href="https://www.openstreetmap.org/user/ {{ row .osm_user | e ( 'url' ) }} " >
2025-06-29 10:22:24 +02:00
{{ row .osm_user }}
</a>
</td>
<td> {{ row .nb }} </td>
<td>
{% if row .completion_moyen is not null %}
{{ row .completion_moyen }} %
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
{% if row .completion_pondere is not null %}
{{ row .completion_pondere }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
{% if row .completion_pondere_normalisee is not null %}
{{ row .completion_pondere_normalisee }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6">Aucun contributeur trouvé pour cette ville.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
2025-06-27 11:14:27 +02:00
</div>
</div>
2025-06-21 18:37:31 +02:00
{ # <div class="card mt-4">
2025-06-17 13:23:47 +02:00
<div class="card-header">
<h2>Requête Overpass</h2>
2025-06-21 18:37:31 +02:00
<div id=overPassRequest >
<pre>
{{ overpass }}
</pre>
</div>
</div>
</div> #}
2025-06-01 18:56:01 +02:00
2025-06-21 12:36:09 +02:00
<div class="completion-info mt-4">
2025-06-23 23:36:50 +02:00
<div class="alert alert-info">
<div class="d-flex align-items-center completion-hover-trigger" style="cursor: pointer;">
<i class="bi bi-info-circle me-2"></i>
<p class="mb-0">Comment est calculé le score de complétion ?</p>
<i class="bi bi-chevron-down ms-auto" id="completionInfoIcon"></i>
2025-06-17 18:27:19 +02:00
</div>
2025-06-23 23:36:50 +02:00
<div id="completionInfoContent" style="display: none;" class="mt-3">
<p>Le score de complétion est calculé en fonction de plusieurs critères :</p>
<ul>
<li>Nom du commerce</li>
<li>Adresse complète (numéro, rue, code postal)</li>
<li>Horaires d'ouverture</li>
<li>Site web</li>
<li>Numéro de téléphone</li>
<li>Accessibilité PMR</li>
2025-06-29 18:32:24 +02:00
<li>SIRET</li>
2025-06-23 23:36:50 +02:00
</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>
</div>
</div>
2025-06-23 23:49:48 +02:00
</div>
2025-06-21 11:28:31 +02:00
<div class="accordion mb-3" id="accordionStats">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
2025-06-21 18:37:31 +02:00
</div>
2025-06-17 18:27:19 +02:00
</div>
2025-06-06 13:26:44 +02:00
2025-07-05 15:25:33 +02:00
<!-- Espace de dump JSON -->
<div id="ctc-json-dump-container" class="mt-4" style="display:none;">
<div class="card">
<div class="card-header p-2">
<i class="bi bi-file-earmark-code"></i> Dump du JSON récupéré
</div>
<div class="card-body p-2">
<pre id="ctc-json-dump" style="max-height:400px;overflow:auto;"></pre>
</div>
</div>
</div>
<div class="mt-3 ctc-tests">
<div class="card ctc-tests">
<div class="card-header p-2">
<i class="bi bi-link-45deg"></i> Tester les JSON Complète tes commerces
</div>
<div class="card-body p-2">
<ul class="mb-0" style="font-size:0.95em;">
{% set ctc_jsons = stats .getAllCTCUrlsMap ( ) %}
{% for key , url in ctc_jsons %}
<li><a href=" {{ url }} " target="_blank" rel="noopener"> {{ key }} </a></li>
{% endfor %}
</ul>
<div class="mt-3">
<div class="input-group">
<select id="ctc-json-select" class="form-select">
<option value="">Choisir un JSON à tester…</option>
{% for key , url in ctc_jsons %}
<option value=" {{ url }} "> {{ key }} </option>
{% endfor %}
</select>
<button id="ctc-json-test-btn" class="btn btn-outline-primary" type="button">
<i class="bi bi-bug"></i> Tester l'accès JSON CTC
</button>
</div>
</div>
</div>
</div>
</div>
<div id="ctc-json-error" class="alert alert-danger mt-4" style="display:none;"></div>
2025-06-29 17:23:54 +02:00
2025-06-18 00:41:24 +02:00
{% endblock %}
{% block javascripts %}
{{ parent ( ) }}
2025-06-23 00:38:13 +02:00
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
2025-06-21 12:36:09 +02:00
document.addEventListener('DOMContentLoaded', function() {
const geojsonData = {{ geojson | raw }} ;
const map_token = " {{ maptiler_token }} ";
2025-07-12 13:32:08 +02:00
// Liste des tags attendus pour la complétion des lieux
const completionTags = {{ completion_tags [ 'places' ] | json_encode | raw }} ;
// Calcul de la complétion et des tags manquants pour chaque lieu
geojsonData.features.forEach(f => {
let filled = 0;
let missing = [];
if (completionTags && completionTags.length > 0) {
completionTags.forEach(tag => {
if (f.properties && typeof f.properties[tag] !== 'undefined' && f.properties[tag] !== null && f.properties[tag] !== '') {
filled++;
} else {
missing.push(tag);
}
});
}
f.properties.completion = completionTags && completionTags.length > 0 ? Math.round(100 * filled / completionTags.length) : null;
// Correction : toujours un tableau
f.properties.missing_tags = Array.isArray(f.properties.missing_tags) ? f.properties.missing_tags : (f.properties.missing_tags ? [f.properties.missing_tags] : []);
});
// Fonction de couleur dégradée
function lerpColor(a, b, t) {
const ah = a.replace('#', '');
const bh = b.replace('#', '');
const ar = parseInt(ah.substring(0,2), 16), ag = parseInt(ah.substring(2,4), 16), ab = parseInt(ah.substring(4,6), 16);
const br = parseInt(bh.substring(0,2), 16), bg = parseInt(bh.substring(2,4), 16), bb = parseInt(bh.substring(4,6), 16);
const rr = Math.round(ar + (br-ar)*t);
const rg = Math.round(ag + (bg-ag)*t);
const rb = Math.round(ab + (bb-ab)*t);
return '#' + rr.toString(16).padStart(2,'0') + rg.toString(16).padStart(2,'0') + rb.toString(16).padStart(2,'0');
}
2025-06-21 12:36:09 +02:00
let map;
2025-06-21 18:37:31 +02:00
let map_is_loaded = false;
let currentMode = 'drop';
// Fonction pour mettre à jour le style des marqueurs
window.updateMarkers = function(mode) {
currentMode = mode;
// Mettre à jour l'état visuel des boutons
document.getElementById('circleMarkersBtn').classList.toggle('active', mode === 'circle');
document.getElementById('dropMarkersBtn').classList.toggle('active', mode === 'drop');
if (!map) return;
// Supprimer la couche si elle existe déjà
if (map.getLayer('unclustered-point')) {
map.removeLayer('unclustered-point');
}
// Ajouter la couche selon le mode
if (mode === 'circle') {
map.addLayer( {
id: 'unclustered-point',
type: 'circle',
source: 'places',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': [
'case',
['has', 'completion'],
[
2025-07-12 13:32:08 +02:00
'interpolate', ['linear'], ['get', 'completion'],
0, '#cccccc',
100, '#008000'
2025-06-21 18:37:31 +02:00
],
'#cccccc'
],
'circle-radius': 10,
'circle-stroke-width': 2,
'circle-stroke-color': '#fff'
}
});
} else {
map.addLayer( {
id: 'unclustered-point',
type: 'circle',
source: 'places',
filter: ['!', ['has', 'point_count']],
paint: {
2025-07-12 13:32:08 +02:00
'circle-color': [
'case',
['has', 'completion'],
[
'interpolate', ['linear'], ['get', 'completion'],
0, '#cccccc',
100, '#008000'
],
'#cccccc'
],
2025-06-21 18:37:31 +02:00
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-stroke-color': '#fff'
}
});
}
}
2025-06-21 12:36:09 +02:00
if (map_token && geojsonData && geojsonData.features.length > 0) {
2025-06-18 00:41:24 +02:00
map = new maplibregl.Map( {
container: 'map',
2025-06-21 12:36:09 +02:00
style: `https://api.maptiler.com/maps/streets/style.json?key=$ { map_token}`,
center: geojsonData.features[0].geometry.coordinates,
zoom: 14
2025-06-18 00:41:24 +02:00
});
2025-06-30 22:27:00 +02:00
window.mapInstance = map;
2025-06-21 12:36:09 +02:00
map.on('load', function() {
map_is_loaded = true;
2025-06-21 18:37:31 +02:00
document.getElementById('maploader').style.display = 'none';
2025-06-18 00:41:24 +02:00
2025-06-21 12:36:09 +02:00
map.addSource('places', {
type: 'geojson',
data: geojsonData,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
2025-06-18 00:41:24 +02:00
});
2025-06-21 12:36:09 +02:00
map.addLayer( {
id: 'clusters',
type: 'circle',
source: 'places',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step', ['get', 'point_count'],
'#51bbd6', 100,
'#f1f075', 750,
'#f28cb1'
],
'circle-radius': [
'step', ['get', 'point_count'],
20, 100,
30, 750,
40
]
}
2025-06-18 00:41:24 +02:00
});
2025-06-21 12:36:09 +02:00
map.addLayer( {
id: 'cluster-count',
type: 'symbol',
source: 'places',
filter: ['has', 'point_count'],
layout: {
'text-field': ' { point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
2025-06-06 12:17:03 +02:00
}
2025-06-21 12:36:09 +02:00
});
2025-06-09 00:34:33 +02:00
2025-06-21 18:37:31 +02:00
// Par défaut, gouttes bleues
window.updateMarkers('drop');
// Listeners boutons
document.getElementById('circleMarkersBtn').addEventListener('click', function() {
window.updateMarkers('circle');
});
document.getElementById('dropMarkersBtn').addEventListener('click', function() {
window.updateMarkers('drop');
2025-06-21 12:36:09 +02:00
});
2025-06-21 18:37:31 +02:00
// Popups et clusters (inchangé)
2025-06-21 12:36:09 +02:00
map.on('click', 'clusters', function(e) {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('places').getClusterExpansionZoom(clusterId, function(err, zoom) {
if (err) return;
map.easeTo( {
center: features[0].geometry.coordinates,
zoom: zoom
});
2025-06-09 00:34:33 +02:00
});
2025-06-21 12:36:09 +02:00
});
2025-06-09 00:34:33 +02:00
2025-06-21 12:36:09 +02:00
map.on('click', 'unclustered-point', function(e) {
const coordinates = e.features[0].geometry.coordinates.slice();
const properties = e.features[0].properties;
2025-06-09 00:34:33 +02:00
2025-06-21 12:36:09 +02:00
let popupContent = `<strong>$ { properties.name || 'Sans nom'}</strong><br>`;
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>`;
2025-07-12 13:32:08 +02:00
popupContent += `<b>Complétion :</b> $ { properties.completion !== null ? properties.completion + '%' : '– '}`;
const missingTags = Array.isArray(properties.missing_tags) ? properties.missing_tags : [];
if (missingTags.length > 0) {
popupContent += `<div style='color:#b30000;font-size:0.95em;margin-top:4px;'><b>Manque :</b> $ { missingTags.map(t => `<code>$ { t}</code>`).join(', ')}</div>`;
}
popupContent += `<br><a href="$ { properties.osm_url}" >Voir sur OSM</a>`;
2025-06-09 00:34:33 +02:00
2025-06-21 12:36:09 +02:00
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
2025-06-04 00:16:56 +02:00
2025-06-21 18:37:31 +02:00
new maplibregl.Popup()
2025-06-21 12:36:09 +02:00
.setLngLat(coordinates)
2025-06-21 18:37:31 +02:00
.setHTML(popupContent)
.addTo(map);
2025-06-06 15:48:03 +02:00
});
2025-06-06 11:50:37 +02:00
2025-06-21 12:36:09 +02:00
map.on('mouseenter', 'clusters', function() {
map.getCanvas().style.cursor = 'pointer';
2025-06-06 15:48:03 +02:00
});
2025-06-21 12:36:09 +02:00
map.on('mouseleave', 'clusters', function() {
2025-06-06 15:48:03 +02:00
map.getCanvas().style.cursor = '';
2025-06-17 16:23:29 +02:00
});
});
2025-06-17 13:23:47 +02:00
} else {
2025-06-21 12:36:09 +02:00
const mapDiv = document.getElementById('map');
if (mapDiv) {
mapDiv.innerHTML = '<div class="alert alert-warning">Aucune donnée géographique à afficher pour cette zone.</div>';
2025-06-18 00:41:24 +02:00
}
2025-06-21 12:36:09 +02:00
document.getElementById('maploader').style.display = 'none';
2025-06-18 00:41:24 +02:00
}
2025-06-18 01:23:16 +02:00
2025-06-21 12:36:09 +02:00
const openInJOSMButton = document.getElementById('openInJOSM');
if (openInJOSMButton) {
openInJOSMButton.addEventListener('click', () => {
2025-06-29 18:32:24 +02:00
const place_nodes = [];
const place_ways = [];
const place_relations = [];
const places = {{ geojson | raw }} .features;
places.forEach(place => {
2025-06-29 19:24:00 +02:00
if (
place.properties.getOsmKind() === 'node'
) {
2025-06-29 18:32:24 +02:00
place_nodes.push(place.properties.id.split('/')[1]);
2025-06-29 19:24:00 +02:00
} else if (
place.properties.getOsmKind() === 'way'
) {
2025-06-29 18:32:24 +02:00
place_ways.push(place.properties.id.split('/')[1]);
}
2025-06-29 19:24:00 +02:00
// elseif (place.properties.getOsmKind() === 'relation') {
// place_relations.push(place.properties.id.split('/')[1]);
// }
2025-06-29 18:32:24 +02:00
});
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);
2025-06-21 12:36:09 +02:00
});
2025-06-18 01:23:16 +02:00
}
2025-06-21 18:37:31 +02:00
// === GRAPHIQUE FRÉQUENCE DES MISES À JOUR PAR TRIMESTRE ===
const modifData = {{ modificationsByQuarter | raw }} ;
const modifLabels = Object.keys(modifData);
const modifCounts = Object.values(modifData);
const modifCanvas = document.getElementById('modificationsByQuarterChart');
if (modifCanvas && modifLabels.length > 0) {
new Chart(modifCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: modifLabels,
datasets: [ {
label: 'Nombre de lieux modifiés',
data: modifCounts,
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
title: { display: true, text: 'Fréquence des mises à jour par trimestre' }
},
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Nombre de lieux' } },
x: { title: { display: true, text: 'Trimestre' } }
}
}
2025-06-24 00:29:15 +02:00
2025-06-21 18:37:31 +02:00
});
} else if (modifCanvas) {
modifCanvas.parentNode.innerHTML = '<div class="alert alert-info">Aucune donnée de modification disponible pour cette ville.</div>';
}
2025-06-23 00:38:13 +02:00
// Créer un graphique de la répartition des tags
const tagsCount = { };
const places = {{ geojson | raw }} .features;
2025-06-18 00:41:24 +02:00
2025-06-23 00:38:13 +02:00
places.forEach(place => {
const mainTag = place.properties.main_tag;
if (mainTag) {
tagsCount[mainTag] = (tagsCount[mainTag] || 0) + 1;
}
});
const sortedTags = Object.entries(tagsCount).sort(([, a], [, b]) => b - a);
const labels = sortedTags.map(item => item[0]);
const data = sortedTags.map(item => item[1]);
const container_tags= document.getElementById('repartition_tags');
2025-06-23 23:49:48 +02:00
console.log('répartition', tagsCount, container_tags)
2025-06-23 00:38:13 +02:00
if(!container_tags) {
console.log('pas de container_tags', container_tags)
return;
}
const ctx = container_tags.getContext ? container_tags.getContext('2d') : null;
if(ctx) {
new Chart(ctx, {
2025-06-23 23:36:50 +02:00
type: 'doughnut',
data: {
labels: labels,
datasets: [ {
label: 'Répartition des tags',
data: data,
backgroundColor: [
'rgba(54, 162, 235, 0.7)',
'rgba(255, 99, 132, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)',
'rgba(201, 203, 207, 0.7)'
],
borderColor: [
'rgba(54, 162, 235, 1)',
'rgba(255, 99, 132, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(201, 203, 207, 1)'
],
borderWidth: 1
}]
2025-06-23 00:38:13 +02:00
},
2025-06-23 23:36:50 +02:00
options: {
2025-06-26 23:14:22 +02:00
responsive: true,
2025-06-23 23:36:50 +02:00
plugins: {
legend: {
position: 'right',
},
title: {
display: true,
text: 'Répartition des tags principaux'
2025-06-23 00:38:13 +02:00
}
2025-06-23 23:36:50 +02:00
}
}
});
}
2025-06-23 00:38:13 +02:00
// Graphique de distribution du taux de complétion
const completionData = [];
{% for commerce in stats .places %}
completionData.push( {{ commerce .getCompletionPercentage ( ) }} );
{% endfor %}
const completionDistribution = { };
completionData.forEach(percent => {
const range = Math.floor(percent / 10) * 10;
const key = `$ { range}-$ { range + 10}%`;
completionDistribution[key] = (completionDistribution[key] || 0) + 1;
});
const completionLabels = Object.keys(completionDistribution).sort((a, b) => {
return parseInt(a.split('-')[0]) - parseInt(b.split('-')[0]);
});
const completionValues = completionLabels.map(label => completionDistribution[label]);
const dc = document.getElementById('distribution_completion');
if(dc ) {
const completionCtx = dc.getContext ? dc.getContext('2d') : null;
if(!completionCtx) {
console.log('pas de completionCtx' )
return ;
}
new Chart(completionCtx, {
2025-06-26 23:14:22 +02:00
type: 'line',
2025-06-29 18:32:24 +02:00
2025-06-23 00:38:13 +02:00
data: {
labels: completionLabels,
2025-06-29 18:32:24 +02:00
tension: 0.3,
2025-06-23 00:38:13 +02:00
datasets: [ {
label: 'Distribution du Taux de Complétion',
data: completionValues,
backgroundColor: 'rgba(75, 192, 192, 0.5)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
},
2025-06-26 23:14:22 +02:00
responsive: true,
2025-06-23 00:38:13 +02:00
}
});
}else {
console.log('pas de distribution_completion')
}
2025-06-26 23:14:22 +02:00
2025-06-23 00:38:13 +02:00
});
</script>
2025-06-23 23:36:50 +02:00
<script>
document.addEventListener('DOMContentLoaded', function() {
const trigger = document.querySelector('.completion-hover-trigger');
const content = document.getElementById('completionInfoContent');
const icon = document.getElementById('completionInfoIcon');
if (trigger && content) {
trigger.addEventListener('mouseenter', function() {
content.style.display = 'block';
if (icon) {
icon.classList.remove('bi-chevron-down');
icon.classList.add('bi-chevron-up');
}
});
trigger.addEventListener('mouseleave', function() {
content.style.display = 'none';
if (icon) {
icon.classList.remove('bi-chevron-up');
icon.classList.add('bi-chevron-down');
}
});
// Pour garder la popup ouverte si la souris va sur la popup
content.addEventListener('mouseenter', function() {
content.style.display = 'block';
});
content.addEventListener('mouseleave', function() {
content.style.display = 'none';
if (icon) {
icon.classList.remove('bi-chevron-up');
icon.classList.add('bi-chevron-down');
}
});
}
});
</script>
2025-06-29 18:32:24 +02:00
<script>
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('btn-geolocate');
btn && btn.addEventListener('click', function() {
2025-06-30 22:27:00 +02:00
console.log('[GEOLOC] Bouton cliqué');
2025-06-29 18:32:24 +02:00
if (!navigator.geolocation) {
alert('La géolocalisation n\'est pas supportée par ce navigateur.');
2025-06-30 22:27:00 +02:00
console.error('[GEOLOC] navigator.geolocation non supporté');
2025-06-29 18:32:24 +02:00
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;
2025-06-30 22:27:00 +02:00
console.log('[GEOLOC] Position obtenue', lat, lon);
2025-06-29 18:32:24 +02:00
// MapLibre
2025-06-30 22:27:00 +02:00
console.log('[GEOLOC] mapInstance', window.mapInstance);
2025-06-29 18:32:24 +02:00
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;
2025-06-30 22:27:00 +02:00
} else {
console.error('[GEOLOC] mapInstance non défini ou flyTo non disponible', window.mapInstance);
2025-06-29 18:32:24 +02:00
}
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-geo-alt"></i> Me localiser';
}, function(err) {
alert('Impossible de vous localiser : ' + err.message);
2025-06-30 22:27:00 +02:00
console.error('[GEOLOC] Erreur de géolocalisation', err);
2025-06-29 18:32:24 +02:00
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-geo-alt"></i> Me localiser';
});
});
});
</script>
2025-07-05 15:25:33 +02:00
<script>
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('ctc-json-test-btn');
const select = document.getElementById('ctc-json-select');
const dumpContainer = document.getElementById('ctc-json-dump-container');
const dump = document.getElementById('ctc-json-dump');
const error = document.getElementById('ctc-json-error');
if(btn && select) {
btn.addEventListener('click', function() {
const url = select.value;
dumpContainer.style.display = 'none';
error.style.display = 'none';
dump.textContent = '';
if(!url) return;
fetch(url)
.then(r => {
if(!r.ok) throw new Error('Erreur HTTP ' + r.status);
return r.json();
})
.then(data => {
dump.textContent = JSON.stringify(data, null, 2);
dumpContainer.style.display = '';
})
.catch(e => {
error.textContent = 'Erreur lors de la récupération du JSON : ' + e.message;
error.style.display = '';
});
});
}
});
</script>
2025-07-14 18:17:41 +02:00
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctcCompletionSeries = {{ ctc_completion_series | json_encode | raw }} ;
console.log('ctcCompletionSeries',ctcCompletionSeries)
// Exemple d'intégration dans un graphique Chart.js :
// Pour chaque type, ajouter une série CTC si elle existe
Object.keys(ctcCompletionSeries).forEach(function(type) {
const data = ctcCompletionSeries[type].map(pt => ( { x: pt.date, y: pt.value }));
// Ajoute la série au graphique correspondant (ex: name_count, hours_count...)
// À adapter selon l'ID du canvas et la structure du graphique
const canvasId = type.replace('_count','') + 'Chart';
const canvas = document.getElementById(canvasId);
if (!canvas) return;
// On suppose que le graphique existe déjà, on ajoute la série CTC
if (canvas.chart) {
canvas.chart.data.datasets.push( {
label: 'CTC (Complète tes commerces)',
data: data,
borderColor: 'orange',
backgroundColor: 'rgba(255,165,0,0.1)',
fill: false,
yAxisID: 'y',
borderDash: [5,3],
datalabels: {
display: false
}
});
canvas.chart.update();
}
});
});
</script>
2025-06-21 11:28:31 +02:00
{% endblock %}