2025-08-31 11:06:54 +02:00
{% extends 'base.html.twig' %}
{% block title %} Carte des problèmes Osmose - {{ city .name }} {% endblock %}
{% block stylesheets %}
{{ parent ( ) }}
<style>
2025-08-31 18:34:19 +02:00
.maplibregl-ctrl-group button {
padding: 1.5rem;
}
.maplibregl-popup {
z-index: 100;
}
2025-08-31 11:06:54 +02:00
#map {
height: 70vh;
width: 100%;
2025-08-31 18:34:19 +02:00
margin-bottom: 430px;
2025-08-31 11:06:54 +02:00
}
.filters {
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
}
.issue-list {
max-height: 500px;
overflow-y: auto;
}
.issue-item {
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
background-color: #f8f9fa;
border-left: 4px solid #007bff;
}
.issue-item.level-1 {
border-left-color: #dc3545; /* Rouge pour les erreurs critiques */
}
.issue-item.level-2 {
border-left-color: #fd7e14; /* Orange pour les erreurs importantes */
}
.issue-item.level-3 {
border-left-color: #ffc107; /* Jaune pour les avertissements */
}
2025-08-31 18:34:19 +02:00
/* Styles de clustering supprimés car le clustering est désactivé */
2025-08-31 11:06:54 +02:00
.marker-level-1 {
background-color: #dc3545;
border-radius: 50%;
width: 100%;
height: 100%;
}
.marker-level-2 {
background-color: #fd7e14;
border-radius: 50%;
width: 100%;
height: 100%;
}
.marker-level-3 {
background-color: #ffc107;
border-radius: 50%;
width: 100%;
height: 100%;
}
.no-issues {
padding: 20px;
text-align: center;
background-color: #f8f9fa;
border-radius: 5px;
margin-top: 20px;
}
</style>
{% endblock %}
{% block body %}
2025-08-31 22:53:28 +02:00
<div class="container-fluid">
<div class="row">
<!-- Sidebar de navigation -->
<div class="col-12 col-lg-3">
{{ stats .name }} :
{% include 'admin/_city_sidebar.html.twig' with { 'stats' : stats , 'active_menu' : 'osmose-dashboard' } %}
</div>
<!-- Contenu principal -->
<div class="col-lg-9 col--12 main-content">
<div class="mt-4">
2025-08-31 11:06:54 +02:00
<h1>Problèmes Osmose pour {{ city .name }} </h1>
<div class="filters">
<form method="get" action=" {{ path ( 'app_admin_osmose_issues_map' , { 'inseeCode' : city .zone } ) }} " class="row">
<div class="col-md-6">
<label for="theme">Filtrer par thème</label>
<select name="theme" id="theme" class="form-select" onchange="this.form.submit()">
<option value="all" {{ theme == 'all' ? 'selected' : '' }} >Tous les thèmes</option>
{% for themeKey , themeLabel in themes %}
2025-08-31 17:57:28 +02:00
{% if themeKey != 'other' or issuesByTheme [ 'other' ] > 0 %}
<option value=" {{ themeKey }} " {{ theme == themeKey ? 'selected' : '' }} >
{{ themeLabel }} {% if issuesByTheme [ themeKey ] > 0 %} ( {{ issuesByTheme [ themeKey ] }} ) {% endif %}
</option>
{% endif %}
2025-08-31 11:06:54 +02:00
{% endfor %}
</select>
</div>
2025-08-31 17:57:28 +02:00
<div class="col-md-6 text-end">
2025-08-31 18:23:41 +02:00
<div class="btn-group">
<a href=" {{ osmoseApiUrl }} " target="_blank" class="btn btn-primary">
<i class="fas fa-external-link-alt"></i> Voir sur Osmose
</a>
<a href=" {{ jsonOsmose }} " target="_blank" class="btn btn-success">
<i class="fas fa-download"></i> Télécharger GeoJSON
</a>
</div>
2025-08-31 17:57:28 +02:00
</div>
2025-08-31 11:06:54 +02:00
</form>
</div>
2025-08-31 17:57:28 +02:00
<!-- Statistiques des alertes -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Répartition par thème</h5>
</div>
<div class="card-body">
{% if osmoseIssues | length > 0 %}
<div class="row">
{% for themeKey , count in issuesByTheme %}
{% if count > 0 %}
<div class="col-md-6 mb-2">
<div class="d-flex justify-content-between">
<span> {{ themes [ themeKey ] }} </span>
<span class="badge bg-primary"> {{ count }} </span>
</div>
<div class="progress">
<div class="progress-bar" role="progressbar"
style="width: {{ ( count / osmoseIssues | length * 1 0 0 ) | round }} %;"
aria-valuenow=" {{ count }} "
aria-valuemin="0"
aria-valuemax=" {{ osmoseIssues | length }} ">
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<p class="text-center">Aucun problème trouvé</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Répartition par niveau de sévérité</h5>
</div>
<div class="card-body">
{% if osmoseIssues | length > 0 %}
<div class="row">
<div class="col-md-12 mb-2">
<div class="d-flex justify-content-between">
<span>Critique</span>
<span class="badge bg-danger"> {{ issuesByLevel [ 1 ] }} </span>
</div>
<div class="progress">
<div class="progress-bar bg-danger" role="progressbar"
style="width: {{ ( issuesByLevel [ 1 ] / osmoseIssues | length * 1 0 0 ) | round }} %;"
aria-valuenow=" {{ issuesByLevel [ 1 ] }} "
aria-valuemin="0"
aria-valuemax=" {{ osmoseIssues | length }} ">
</div>
</div>
</div>
<div class="col-md-12 mb-2">
<div class="d-flex justify-content-between">
<span>Important</span>
<span class="badge bg-warning text-dark"> {{ issuesByLevel [ 2 ] }} </span>
</div>
<div class="progress">
<div class="progress-bar bg-warning" role="progressbar"
style="width: {{ ( issuesByLevel [ 2 ] / osmoseIssues | length * 1 0 0 ) | round }} %;"
aria-valuenow=" {{ issuesByLevel [ 2 ] }} "
aria-valuemin="0"
aria-valuemax=" {{ osmoseIssues | length }} ">
</div>
</div>
</div>
<div class="col-md-12 mb-2">
<div class="d-flex justify-content-between">
<span>Avertissement</span>
<span class="badge bg-info"> {{ issuesByLevel [ 3 ] }} </span>
</div>
<div class="progress">
<div class="progress-bar bg-info" role="progressbar"
style="width: {{ ( issuesByLevel [ 3 ] / osmoseIssues | length * 1 0 0 ) | round }} %;"
aria-valuenow=" {{ issuesByLevel [ 3 ] }} "
aria-valuemin="0"
aria-valuemax=" {{ osmoseIssues | length }} ">
</div>
</div>
</div>
</div>
{% else %}
<p class="text-center">Aucun problème trouvé</p>
{% endif %}
</div>
</div>
</div>
</div>
2025-08-31 11:06:54 +02:00
<div id="map"></div>
2025-08-31 17:57:28 +02:00
<div class="row mt-4">
2025-08-31 11:06:54 +02:00
<div class="col-md-12">
2025-08-31 17:57:28 +02:00
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h3 class="mb-0">Liste des problèmes ( {{ osmoseIssues | length }} )</h3>
<div class="btn-group">
<button class="btn btn-sm btn-light" id="sort-by-level">Trier par sévérité</button>
<button class="btn btn-sm btn-light" id="sort-by-item">Trier par type</button>
</div>
2025-08-31 11:06:54 +02:00
</div>
2025-08-31 17:57:28 +02:00
<div class="card-body">
{% if osmoseIssues | length > 0 %}
<div class="issue-list">
{% for issue in osmoseIssues %}
<div class="issue-item level- {{ issue .level }} " data-lat=" {{ issue .lat }} " data-lon=" {{ issue .lon }} " data-level=" {{ issue .level }} " data-item=" {{ issue .item }} ">
<div class="row">
<div class="col-md-9">
<h5> {{ issue .title }} </h5>
{% if issue .subtitle %}
<p class="text-muted"> {{ issue .subtitle }} </p>
{% endif %}
<div class="d-flex gap-2 mt-2">
<span class="badge bg-secondary">Item: {{ issue .item }} </span>
{% if issue .level == 1 %}
<span class="badge bg-danger">Critique</span>
{% elseif issue .level == 2 %}
<span class="badge bg-warning text-dark">Important</span>
{% elseif issue .level == 3 %}
<span class="badge bg-info">Avertissement</span>
{% endif %}
</div>
</div>
<div class="col-md-3 text-end d-flex flex-column justify-content-between">
<a href=" {{ issue .url }} " target="_blank" class="btn btn-sm btn-primary mb-2">
<i class="fas fa-external-link-alt"></i> Voir sur Osmose
</a>
<button class="btn btn-sm btn-outline-secondary locate-on-map">
<i class="fas fa-map-marker-alt"></i> Localiser sur la carte
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="no-issues">
<p class="text-center">Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</p>
</div>
{% endif %}
2025-08-31 11:06:54 +02:00
</div>
2025-08-31 17:57:28 +02:00
</div>
2025-08-31 11:06:54 +02:00
</div>
</div>
</div>
2025-08-31 22:53:28 +02:00
</div>
</div>
</div>
2025-08-31 11:06:54 +02:00
{% endblock %}
{% block javascripts %}
{{ parent ( ) }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialiser la carte avec MapLibre
const map = new maplibregl.Map( {
container: 'map',
2025-08-31 18:23:41 +02:00
style: 'https://api.maptiler.com/maps/streets/style.json?key= {{ maptiler_token | default ( "" ) }} ', // Utiliser MapTiler si disponible
2025-08-31 11:06:54 +02:00
center: [ {{ city .lon }} , {{ city .lat }} ], // Note: MapLibre uses [longitude, latitude] order
zoom: 13
});
2025-08-31 18:23:41 +02:00
// Gérer les erreurs de chargement de la carte
map.on('error', function(e) {
console.error('Erreur de chargement de la carte:', e.error);
document.getElementById('map').innerHTML = '<div class="alert alert-danger">Erreur de chargement de la carte. Veuillez réessayer plus tard.</div>';
});
2025-08-31 11:06:54 +02:00
// Ajouter les contrôles de navigation
map.addControl(new maplibregl.NavigationControl());
// Attendre que la carte soit chargée
map.on('load', function() {
// Créer une source de données pour les marqueurs
const features = [];
// Ajouter les marqueurs pour chaque problème
{% for issue in osmoseIssues %}
features.push( {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ {{ issue .lon }} , {{ issue .lat }} ]
},
properties: {
title: " {{ issue .title | e ( 'js' ) }} ",
subtitle: " {{ issue .subtitle | e ( 'js' ) }} ",
item: " {{ issue .item }} ",
url: " {{ issue .url }} ",
level: " {{ issue .level }} "
}
});
{% endfor %}
2025-08-31 18:23:41 +02:00
// Vérifier s'il y a des problèmes à afficher
if (features.length === 0) {
// Ajouter un message si aucun problème n'est trouvé
const mapContainer = document.getElementById('map');
mapContainer.insertAdjacentHTML('afterend', '<div class="alert alert-info mt-3">Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</div>');
}
2025-08-31 11:06:54 +02:00
// Ajouter la source de données à la carte
map.addSource('issues', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: features
},
2025-08-31 18:34:19 +02:00
cluster: false // Désactiver le clustering comme demandé
2025-08-31 11:06:54 +02:00
});
2025-08-31 18:34:19 +02:00
// Avec le clustering désactivé, nous n'avons plus besoin des couches de clusters
2025-08-31 11:06:54 +02:00
2025-08-31 18:34:19 +02:00
// Ajouter une couche pour les points (sans filtre de clustering puisqu'il est désactivé)
2025-08-31 11:06:54 +02:00
map.addLayer( {
2025-08-31 18:34:19 +02:00
id: 'point',
2025-08-31 11:06:54 +02:00
type: 'circle',
source: 'issues',
paint: {
'circle-color': [
'match',
['get', 'level'],
'1', '#dc3545',
'2', '#fd7e14',
'3', '#ffc107',
'#007bff'
],
'circle-radius': 10,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
2025-08-31 18:34:19 +02:00
// Avec le clustering désactivé, nous n'avons plus besoin de l'événement de clic sur les clusters
2025-08-31 11:06:54 +02:00
// Ajouter un événement de clic sur les points individuels
2025-08-31 18:34:19 +02:00
map.on('click', 'point', function(e) {
2025-08-31 11:06:54 +02:00
const coordinates = e.features[0].geometry.coordinates.slice();
2025-08-31 18:34:19 +02:00
const properties = e.features[0].properties;
// Extraire toutes les propriétés disponibles
const id = properties.id || 'N/A';
const title = properties.title || 'Problème sans titre';
const subtitle = properties.subtitle || '';
const lat = e.features[0].geometry.coordinates[1];
const lon = e.features[0].geometry.coordinates[0];
const item = properties.item || '';
const itemClass = properties.class || '';
const level = properties.level || '';
const updateTimestamp = properties.update_timestamp ? new Date(properties.update_timestamp * 1000).toLocaleString() : 'N/A';
const url = properties.url || '#';
2025-08-31 11:06:54 +02:00
2025-08-31 18:34:19 +02:00
// Créer le contenu de la popup avec toutes les propriétés
2025-08-31 11:06:54 +02:00
let popupContent = `
2025-08-31 18:34:19 +02:00
<div class="popup-content" style="max-width: 300px;">
<h5>$ { title}</h5>
2025-08-31 11:06:54 +02:00
`;
if (subtitle) {
2025-08-31 18:34:19 +02:00
popupContent += `<p class="text-muted">$ { subtitle}</p>`;
2025-08-31 11:06:54 +02:00
}
2025-08-31 18:34:19 +02:00
// Ajouter toutes les propriétés dans un tableau
2025-08-31 11:06:54 +02:00
popupContent += `
2025-08-31 18:34:19 +02:00
<table class="table table-sm table-bordered mt-2">
<tbody>
<tr>
<th>ID</th>
<td>$ { id}</td>
</tr>
<tr>
<th>Coordonnées</th>
<td>$ { lat.toFixed(6)}, $ { lon.toFixed(6)}</td>
</tr>
<tr>
<th>Item</th>
<td>$ { item}</td>
</tr>
<tr>
<th>Classe</th>
<td>$ { itemClass}</td>
</tr>
<tr>
<th>Niveau</th>
<td>
<span class="badge $ { level == 1 ? 'bg-danger' : level == 2 ? 'bg-warning text-dark' : 'bg-info'}">
$ { level == 1 ? 'Critique' : level == 2 ? 'Important' : 'Avertissement'}
</span>
</td>
</tr>
<tr>
<th>Dernière mise à jour</th>
<td>$ { updateTimestamp}</td>
</tr>
</tbody>
</table>
2025-08-31 11:06:54 +02:00
<div class="mt-2">
<a href="$ { url}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
</div>
2025-08-31 18:34:19 +02:00
</div>
2025-08-31 11:06:54 +02:00
`;
// Assurer que si le zoom change, la popup reste à la bonne position
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
new maplibregl.Popup()
.setLngLat(coordinates)
.setHTML(popupContent)
.addTo(map);
});
2025-08-31 18:34:19 +02:00
// Changer le curseur au survol des points
map.on('mouseenter', 'point', function() {
2025-08-31 11:06:54 +02:00
map.getCanvas().style.cursor = 'pointer';
});
2025-08-31 18:34:19 +02:00
map.on('mouseleave', 'point', function() {
2025-08-31 11:06:54 +02:00
map.getCanvas().style.cursor = '';
});
2025-08-31 17:57:28 +02:00
// Fonction pour localiser un problème sur la carte
function locateIssueOnMap(lat, lon) {
map.flyTo( {
center: [lon, lat],
zoom: 18
});
// Simuler un clic sur le point pour ouvrir la popup
const features = map.queryRenderedFeatures(
map.project([lon, lat]),
2025-08-31 18:34:19 +02:00
{ layers: ['point'] }
2025-08-31 17:57:28 +02:00
);
if (features.length > 0) {
map.fire('click', {
lngLat: { lng: lon, lat: lat },
point: map.project([lon, lat]),
features: [features[0]]
2025-08-31 11:06:54 +02:00
});
2025-08-31 17:57:28 +02:00
}
}
// Ajouter un événement de clic sur les boutons "Localiser sur la carte"
document.querySelectorAll('.locate-on-map').forEach(function(button) {
button.addEventListener('click', function(e) {
e.stopPropagation(); // Empêcher la propagation au parent
const issueItem = this.closest('.issue-item');
const lat = parseFloat(issueItem.dataset.lat);
const lon = parseFloat(issueItem.dataset.lon);
locateIssueOnMap(lat, lon);
2025-08-31 11:06:54 +02:00
});
2025-08-31 17:57:28 +02:00
});
// Ajouter un événement de clic sur les éléments de la liste
document.querySelectorAll('.issue-item').forEach(function(item) {
item.addEventListener('click', function() {
const lat = parseFloat(this.dataset.lat);
const lon = parseFloat(this.dataset.lon);
locateIssueOnMap(lat, lon);
});
});
2025-08-31 11:06:54 +02:00
// Ajuster la vue pour montrer tous les marqueurs si nécessaire
if (features.length > 0) {
// Calculer les limites de tous les points
const bounds = new maplibregl.LngLatBounds();
features.forEach(function(feature) {
bounds.extend(feature.geometry.coordinates);
});
map.fitBounds(bounds, {
padding: 50
});
}
2025-08-31 17:57:28 +02:00
// Fonctions de tri pour les problèmes
function sortIssuesByLevel() {
const issueList = document.querySelector('.issue-list');
const issues = Array.from(issueList.querySelectorAll('.issue-item'));
issues.sort(function(a, b) {
return parseInt(a.dataset.level) - parseInt(b.dataset.level);
});
issues.forEach(function(issue) {
issueList.appendChild(issue);
});
}
function sortIssuesByItem() {
const issueList = document.querySelector('.issue-list');
const issues = Array.from(issueList.querySelectorAll('.issue-item'));
issues.sort(function(a, b) {
return parseInt(a.dataset.item) - parseInt(b.dataset.item);
});
issues.forEach(function(issue) {
issueList.appendChild(issue);
});
}
// Ajouter des événements de clic sur les boutons de tri
document.getElementById('sort-by-level').addEventListener('click', sortIssuesByLevel);
document.getElementById('sort-by-item').addEventListener('click', sortIssuesByItem);
2025-08-31 11:06:54 +02:00
});
});
</script>
{% endblock %}