diff --git a/loko/assets/migrations/0027_alter_structure_structure_type.py b/loko/assets/migrations/0027_alter_structure_structure_type.py
new file mode 100644
index 0000000..fc1ee09
--- /dev/null
+++ b/loko/assets/migrations/0027_alter_structure_structure_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 6.0.7 on 2026-09-21 11:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assets', '0026_add_naturetree_specific_fields'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='structure',
+ name='structure_type',
+ field=models.CharField(blank=True, choices=[('tunnel', 'Tunnel'), ('bridge', 'Pont'), ('viaduct', 'Viaduc'), ('overpass', 'Passerelle'), ('underpass', 'Passage souterrain'), ('gallery', 'Galerie technique'), ('atypical', 'Ouvrage atypique'), ('metro_station', 'Station de métro'), ('metro_tunnel', 'Tunnel de métro')], max_length=200, null=True),
+ ),
+ ]
diff --git a/loko/assets/models/structures.py b/loko/assets/models/structures.py
index edbc81e..cfc62f6 100644
--- a/loko/assets/models/structures.py
+++ b/loko/assets/models/structures.py
@@ -17,13 +17,15 @@ private_storage = PrivateMediaStorage()
STRUCTURE_TYPE_CHOICES = (
- ('tunnel', 'Tunnnel'),
- ('bridge', 'Pont'),
- ('viaduct', 'Viaduc'),
- ('overpass', 'Passerelle'),
- ('underpass', 'Passage souterrain'),
- ('gallery', 'Galerie technique'),
- ('atypical', 'Ouvrage atypique'),
+ ('tunnel', _('Tunnel')),
+ ('bridge', _('Pont')),
+ ('viaduct', _('Viaduc')),
+ ('overpass', _('Passerelle')),
+ ('underpass', _('Passage souterrain')),
+ ('gallery', _('Galerie technique')),
+ ('atypical', _('Ouvrage atypique')),
+ ('metro_station', _('Station de métro')),
+ ('metro_tunnel', _('Tunnel de métro')),
)
diff --git a/loko/assets/views/structures.py b/loko/assets/views/structures.py
index 2b88e5c..c393a03 100644
--- a/loko/assets/views/structures.py
+++ b/loko/assets/views/structures.py
@@ -61,7 +61,7 @@ def structures_index(request):
selected_locations_ids = draft_data.get('locations', [])
- user_config, _ = UserConfig.objects.get_or_create(user=request.user)
+ user_config, _created = UserConfig.objects.get_or_create(user=request.user)
thematic_choices_qs = (
UserThematics.objects
.filter(user_config=user_config, can_view_assets=True)
diff --git a/loko/home/templates/home/widgets/exceptional_transport.html b/loko/home/templates/home/widgets/exceptional_transport.html
index 417d0e0..aa141f3 100644
--- a/loko/home/templates/home/widgets/exceptional_transport.html
+++ b/loko/home/templates/home/widgets/exceptional_transport.html
@@ -31,10 +31,10 @@
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
{% translate "Types d'ouvrage" %}
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -157,6 +191,14 @@
padding: 6px 10px;
text-align: center;
}
+.te-type-item {
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out;
+ user-select: none;
+}
+.te-type-item:hover {
+ background-color: #f1f8ff;
+}
@@ -165,6 +207,9 @@
const GEOJSON_URL = "{% url 'assets:structures_exceptional_transport_geojson' %}";
let teMap = null;
let teFeatures = [];
+ let visibleFeatures = [];
+ let availableTypes = [];
+ let selectedTypeCodes = new Set();
let activeFilter = 'all';
let currentPopup = null;
@@ -198,6 +243,11 @@
const data = await response.json();
teFeatures = (data && data.features) ? data.features : [];
+ // Discover available structure types & select all except metro tunnels by default
+ initStructureTypes();
+ updateVisibleFeatures();
+
+ setupTypeFilterMenu();
updateCounts();
renderMapLayers();
setupSearchAutocomplete();
@@ -216,11 +266,136 @@
});
}
+ function initStructureTypes() {
+ const typeMap = new Map();
+ teFeatures.forEach(f => {
+ const code = f.properties.structure_type_code || 'other';
+ const label = f.properties.structure_type || (code === 'other' ? "{% translate 'Autre' %}" : code);
+ if (!typeMap.has(code)) {
+ typeMap.set(code, { code: code, label: label, count: 0 });
+ }
+ typeMap.get(code).count++;
+ });
+
+ availableTypes = Array.from(typeMap.values()).sort((a, b) => a.label.localeCompare(b.label, 'fr'));
+
+ // Default: include all structure types EXCEPT metro tunnels ('metro_tunnel')
+ selectedTypeCodes = new Set();
+ availableTypes.forEach(t => {
+ if (t.code !== 'metro_tunnel') {
+ selectedTypeCodes.add(t.code);
+ }
+ });
+ }
+
+ function updateVisibleFeatures() {
+ visibleFeatures = teFeatures.filter(f => {
+ const code = f.properties.structure_type_code || 'other';
+ return selectedTypeCodes.has(code);
+ });
+ }
+
+ function setupTypeFilterMenu() {
+ const listContainer = document.getElementById('te-type-checkbox-list');
+ if (!listContainer) return;
+
+ listContainer.innerHTML = '';
+ availableTypes.forEach(t => {
+ const isChecked = selectedTypeCodes.has(t.code);
+ const item = document.createElement('div');
+ item.className = 'dropdown-item d-flex align-items-center justify-content-between rounded-2 px-2 py-1 te-type-item';
+ item.innerHTML = `
+
+
+
+
+
${t.count}
+ `;
+
+ const cb = item.querySelector('.te-type-checkbox');
+ cb.addEventListener('change', () => {
+ if (cb.checked) {
+ selectedTypeCodes.add(t.code);
+ } else {
+ selectedTypeCodes.delete(t.code);
+ }
+ onTypeFilterChanged();
+ });
+
+ item.addEventListener('click', (e) => {
+ if (e.target !== cb && e.target.tagName !== 'LABEL') {
+ cb.checked = !cb.checked;
+ cb.dispatchEvent(new Event('change'));
+ }
+ });
+
+ listContainer.appendChild(item);
+ });
+
+ // Quick action: Select All
+ const btnAll = document.getElementById('te-types-select-all');
+ if (btnAll) {
+ btnAll.addEventListener('click', (e) => {
+ e.preventDefault();
+ availableTypes.forEach(t => selectedTypeCodes.add(t.code));
+ document.querySelectorAll('.te-type-checkbox').forEach(cb => cb.checked = true);
+ onTypeFilterChanged();
+ });
+ }
+
+ // Quick action: Select None
+ const btnNone = document.getElementById('te-types-select-none');
+ if (btnNone) {
+ btnNone.addEventListener('click', (e) => {
+ e.preventDefault();
+ selectedTypeCodes.clear();
+ document.querySelectorAll('.te-type-checkbox').forEach(cb => cb.checked = false);
+ onTypeFilterChanged();
+ });
+ }
+
+ updateTypeFilterBadge();
+ }
+
+ function updateTypeFilterBadge() {
+ const badge = document.getElementById('te-types-count-badge');
+ if (badge) {
+ badge.textContent = `${selectedTypeCodes.size}/${availableTypes.length}`;
+ if (selectedTypeCodes.size === availableTypes.length) {
+ badge.className = 'badge bg-primary rounded-pill ms-1';
+ } else if (selectedTypeCodes.size === 0) {
+ badge.className = 'badge bg-danger rounded-pill ms-1';
+ } else {
+ badge.className = 'badge bg-primary rounded-pill ms-1';
+ }
+ }
+ }
+
+ function onTypeFilterChanged() {
+ updateVisibleFeatures();
+ updateTypeFilterBadge();
+
+ if (teMap && teMap.getSource('te-structures')) {
+ teMap.getSource('te-structures').setData({
+ type: 'FeatureCollection',
+ features: visibleFeatures
+ });
+ }
+
+ updateCounts();
+
+ if (currentPopup) {
+ currentPopup.remove();
+ }
+ }
+
function updateCounts() {
- let total = teFeatures.length;
+ let total = visibleFeatures.length;
let auth = 0, cond = 0, notEval = 0, restricted = 0;
- teFeatures.forEach(f => {
+ visibleFeatures.forEach(f => {
const st = f.properties.passage_status;
if (st === 'authorized') auth++;
else if (st === 'conditional') cond++;
@@ -247,7 +422,7 @@
const geojsonData = {
type: 'FeatureCollection',
- features: teFeatures
+ features: visibleFeatures
};
teMap.addSource('te-structures', {
@@ -551,7 +726,7 @@
}
// Filter features
- const matches = teFeatures.filter(f => {
+ const matches = visibleFeatures.filter(f => {
const p = f.properties;
const st = p.passage_status;
@@ -608,7 +783,7 @@
dropdown.querySelectorAll('.te-autocomplete-item').forEach(item => {
item.addEventListener('click', () => {
const sid = parseInt(item.getAttribute('data-structure-id'));
- const selected = teFeatures.find(f => f.properties.structure_id === sid);
+ const selected = visibleFeatures.find(f => f.properties.structure_id === sid);
if (selected && teMap) {
const coords = selected.geometry.coordinates;
teMap.flyTo({