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 @@
- +
-
+
@@ -55,24 +55,58 @@
- -
-
- - - - - + +
+
+ + + + +
+ + + + + +
@@ -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({