osm-commerces/templates/admin/osmose_issues_map.html.twig

353 lines
No EOL
14 KiB
Twig

{% extends 'base.html.twig' %}
{% block title %}Carte des problèmes Osmose - {{ city.name }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
#map {
height: 70vh;
width: 100%;
margin-bottom: 20px;
}
.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 */
}
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
.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 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 %}
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
{{ themeLabel }}
</option>
{% endfor %}
</select>
</div>
</form>
</div>
<div id="map"></div>
<div class="row">
<div class="col-md-12">
<h3>Liste des problèmes ({{ osmoseIssues|length }})</h3>
{% 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 }}">
<h5>{{ issue.title }}</h5>
{% if issue.subtitle %}
<p>{{ issue.subtitle }}</p>
{% endif %}
<div class="d-flex justify-content-between">
<span class="badge bg-secondary">Item: {{ issue.item }}</span>
<a href="{{ issue.url }}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="no-issues">
<p>Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</p>
</div>
{% endif %}
</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://demotiles.maplibre.org/style.json', // style URL
center: [{{ city.lon }}, {{ city.lat }}], // Note: MapLibre uses [longitude, latitude] order
zoom: 13
});
// 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 %}
// Ajouter la source de données à la carte
map.addSource('issues', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: features
},
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
// Ajouter une couche pour les clusters
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'issues',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'rgba(181, 226, 140, 0.6)',
10,
'rgba(241, 211, 87, 0.6)',
30,
'rgba(253, 156, 115, 0.6)'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
10,
30,
30,
40
]
}
});
// Ajouter une couche pour le nombre de points dans chaque cluster
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'issues',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
});
// Ajouter une couche pour les points individuels
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'issues',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': [
'match',
['get', 'level'],
'1', '#dc3545',
'2', '#fd7e14',
'3', '#ffc107',
'#007bff'
],
'circle-radius': 10,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
// Ajouter un événement de clic sur les clusters
map.on('click', 'clusters', function(e) {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('issues').getClusterExpansionZoom(clusterId, function(err, zoom) {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
});
});
// Ajouter un événement de clic sur les points individuels
map.on('click', 'unclustered-point', function(e) {
const coordinates = e.features[0].geometry.coordinates.slice();
const title = e.features[0].properties.title;
const subtitle = e.features[0].properties.subtitle;
const item = e.features[0].properties.item;
const url = e.features[0].properties.url;
// Créer le contenu de la popup
let popupContent = `
<h5>${title}</h5>
`;
if (subtitle) {
popupContent += `<p>${subtitle}</p>`;
}
popupContent += `
<div>
<span class="badge bg-secondary">Item: ${item}</span>
</div>
<div class="mt-2">
<a href="${url}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
</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 clusters et des points
map.on('mouseenter', 'clusters', function() {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', function() {
map.getCanvas().style.cursor = '';
});
map.on('mouseenter', 'unclustered-point', function() {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'unclustered-point', function() {
map.getCanvas().style.cursor = '';
});
// Ajouter un événement de clic sur les éléments de la liste
{% for issue in osmoseIssues %}
document.querySelector(`.issue-item[data-lat="{{ issue.lat }}"][data-lon="{{ issue.lon }}"]`)?.addEventListener('click', function() {
map.flyTo({
center: [{{ issue.lon }}, {{ issue.lat }}],
zoom: 18
});
// Simuler un clic sur le point pour ouvrir la popup
const features = map.queryRenderedFeatures(
map.project([{{ issue.lon }}, {{ issue.lat }}]),
{ layers: ['unclustered-point'] }
);
if (features.length > 0) {
map.fire('click', {
lngLat: { lng: {{ issue.lon }}, lat: {{ issue.lat }} },
point: map.project([{{ issue.lon }}, {{ issue.lat }}]),
features: [features[0]]
});
}
});
{% endfor %}
// 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
});
}
});
});
</script>
{% endblock %}