534 lines
No EOL
26 KiB
Twig
534 lines
No EOL
26 KiB
Twig
{% extends 'base.html.twig' %}
|
|
|
|
{% block title %}Carte des problèmes Osmose - {{ city.name }}{% endblock %}
|
|
|
|
{% block stylesheets %}
|
|
{{ parent() }}
|
|
<style>
|
|
|
|
.maplibregl-ctrl-group button{
|
|
padding: 1.5rem;
|
|
|
|
}
|
|
.maplibregl-popup{
|
|
z-index: 100;
|
|
}
|
|
#map {
|
|
height: 70vh;
|
|
width: 100%;
|
|
margin-bottom: 430px;
|
|
}
|
|
.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 */
|
|
}
|
|
/* Styles de clustering supprimés car le clustering est désactivé */
|
|
.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 %}
|
|
<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">
|
|
<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 %}
|
|
{% if themeKey != 'other' or issuesByTheme['other'] > 0 %}
|
|
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
|
{{ themeLabel }}{% if issuesByTheme[themeKey] > 0 %} ({{ issuesByTheme[themeKey] }}){% endif %}
|
|
</option>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<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>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 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 * 100)|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 * 100)|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 * 100)|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 * 100)|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>
|
|
|
|
<div id="map"></div>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-md-12">
|
|
<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>
|
|
</div>
|
|
<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 %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block javascripts %}
|
|
{{ parent() }}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialiser la carte avec MapLibre
|
|
const map = new maplibregl.Map({
|
|
container: 'map',
|
|
style: 'https://api.maptiler.com/maps/streets/style.json?key={{ maptiler_token|default("") }}', // Utiliser MapTiler si disponible
|
|
center: [{{ city.lon }}, {{ city.lat }}], // Note: MapLibre uses [longitude, latitude] order
|
|
zoom: 13
|
|
});
|
|
|
|
// 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>';
|
|
});
|
|
|
|
// 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 %}
|
|
|
|
// 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>');
|
|
}
|
|
|
|
// Ajouter la source de données à la carte
|
|
map.addSource('issues', {
|
|
type: 'geojson',
|
|
data: {
|
|
type: 'FeatureCollection',
|
|
features: features
|
|
},
|
|
cluster: false // Désactiver le clustering comme demandé
|
|
});
|
|
|
|
// Avec le clustering désactivé, nous n'avons plus besoin des couches de clusters
|
|
|
|
// Ajouter une couche pour les points (sans filtre de clustering puisqu'il est désactivé)
|
|
map.addLayer({
|
|
id: 'point',
|
|
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'
|
|
}
|
|
});
|
|
|
|
// Avec le clustering désactivé, nous n'avons plus besoin de l'événement de clic sur les clusters
|
|
|
|
// Ajouter un événement de clic sur les points individuels
|
|
map.on('click', 'point', function(e) {
|
|
const coordinates = e.features[0].geometry.coordinates.slice();
|
|
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 || '#';
|
|
|
|
// Créer le contenu de la popup avec toutes les propriétés
|
|
let popupContent = `
|
|
<div class="popup-content" style="max-width: 300px;">
|
|
<h5>${title}</h5>
|
|
`;
|
|
|
|
if (subtitle) {
|
|
popupContent += `<p class="text-muted">${subtitle}</p>`;
|
|
}
|
|
|
|
// Ajouter toutes les propriétés dans un tableau
|
|
popupContent += `
|
|
<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>
|
|
<div class="mt-2">
|
|
<a href="${url}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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);
|
|
});
|
|
|
|
// Changer le curseur au survol des points
|
|
map.on('mouseenter', 'point', function() {
|
|
map.getCanvas().style.cursor = 'pointer';
|
|
});
|
|
map.on('mouseleave', 'point', function() {
|
|
map.getCanvas().style.cursor = '';
|
|
});
|
|
|
|
// 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]),
|
|
{ layers: ['point'] }
|
|
);
|
|
|
|
if (features.length > 0) {
|
|
map.fire('click', {
|
|
lngLat: { lng: lon, lat: lat },
|
|
point: map.project([lon, lat]),
|
|
features: [features[0]]
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
|
|
// 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
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |