feat: add basemap preference persistence and layer switcher control support

This commit is contained in:
kdeterme 2026-10-09 14:16:12 +02:00
parent 9bdd68bea2
commit e65c402395
11 changed files with 116 additions and 15 deletions

View file

@ -748,6 +748,14 @@ function applyInitialPreferences() {
window._restoredMapCenter = prefs.map_center; window._restoredMapCenter = prefs.map_center;
window._restoredMapZoom = prefs.map_zoom; window._restoredMapZoom = prefs.map_zoom;
} }
// Basemap preference
if (prefs.basemap) {
window._restoredBasemap = prefs.basemap;
try {
localStorage.setItem('loko_active_basemap', prefs.basemap);
} catch (e) {}
}
} else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) { } else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) {
// First visit: pre-select the user's default thematic // First visit: pre-select the user's default thematic
activeThematics = new Set([DEFAULT_THEMATIC_CODE]); activeThematics = new Set([DEFAULT_THEMATIC_CODE]);
@ -775,6 +783,9 @@ function savePreferences() {
).map(cb => cb.value); ).map(cb => cb.value);
const includeNoGeomCheckbox = document.getElementById('include-no-geom'); const includeNoGeomCheckbox = document.getElementById('include-no-geom');
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true; const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
const activeBasemap = (window.unifiedLayerSwitcher && window.unifiedLayerSwitcher.activeBasemapId)
|| localStorage.getItem('loko_active_basemap')
|| 'default';
const prefs = { const prefs = {
thematics: Array.from(activeThematics), thematics: Array.from(activeThematics),
categories: Array.from(activeCategories), categories: Array.from(activeCategories),
@ -787,6 +798,7 @@ function savePreferences() {
include_no_geom: includeNoGeom, include_no_geom: includeNoGeom,
map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null, map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null,
map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null, map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null,
basemap: activeBasemap,
}; };
fetch(SAVE_ASSETS_PREFS_URL, { fetch(SAVE_ASSETS_PREFS_URL, {
method: 'POST', method: 'POST',
@ -2929,7 +2941,12 @@ maploader.addEventListener('mapLoaded', () => {
// Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost) // Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost)
if (typeof LayerSwitcherControl !== 'undefined') { if (typeof LayerSwitcherControl !== 'undefined') {
const initialBasemap = window._restoredBasemap || localStorage.getItem('loko_active_basemap') || 'default';
const layerSwitcher = new LayerSwitcherControl({ const layerSwitcher = new LayerSwitcherControl({
initialBasemapId: initialBasemap,
onBasemapChange: function (basemapId) {
schedulePreferencesSave();
},
layers: [{ layers: [{
id: 'basemap', id: 'basemap',
name: 'Fonds de plan & Orthophotos', name: 'Fonds de plan & Orthophotos',
@ -2937,7 +2954,10 @@ maploader.addEventListener('mapLoaded', () => {
beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'), beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'),
basemaps: window.MAP_BACKGROUNDS || [], basemaps: window.MAP_BACKGROUNDS || [],
}], }],
});// Reset view — fly back to initial Brussels Region view });
window.unifiedLayerSwitcher = layerSwitcher;
// Reset view — fly back to initial Brussels Region view
map.addControl({ map.addControl({
onAdd(m) { onAdd(m) {
this._map = m; this._map = m;

View file

@ -1315,7 +1315,7 @@
{% block script %} {% block script %}
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script> <script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
<script src="{% static 'common/layer-switcher-control.js' %}"></script> <script src="{% static 'common/layer-switcher-control.js' %}?v=20261009_basemap"></script>
<script> <script>
window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}"; window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}";
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}"; const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
@ -1374,5 +1374,5 @@
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script> <script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
<script src="{% static 'assets/asset_symbology.js' %}?v=20261009_tree_icons"></script> <script src="{% static 'assets/asset_symbology.js' %}?v=20261009_tree_icons"></script>
<script src="{% static 'assets/unified_map_layers.js' %}?v=20261009_tree_icons"></script> <script src="{% static 'assets/unified_map_layers.js' %}?v=20261009_tree_icons"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20261009_tree_icons"></script> <script src="{% static 'assets/unified_index.js' %}?v=20261009_basemap"></script>
{% endblock script %} {% endblock script %}

View file

@ -553,3 +553,8 @@ msgstr "Zoom in"
#: common/templates/common/components/document_viewer.html #: common/templates/common/components/document_viewer.html
msgid "Réinitialiser" msgid "Réinitialiser"
msgstr "Reset" msgstr "Reset"
#: common/views.py:683
msgid "Format de fond de plan invalide"
msgstr "Invalid basemap format"

View file

@ -539,3 +539,8 @@ msgstr "Zoomer"
#: common/templates/common/components/document_viewer.html #: common/templates/common/components/document_viewer.html
msgid "Réinitialiser" msgid "Réinitialiser"
msgstr "Réinitialiser" msgstr "Réinitialiser"
#: common/views.py:683
msgid "Format de fond de plan invalide"
msgstr "Format de fond de plan invalide"

View file

@ -539,3 +539,8 @@ msgstr "Inzoomen"
#: common/templates/common/components/document_viewer.html #: common/templates/common/components/document_viewer.html
msgid "Réinitialiser" msgid "Réinitialiser"
msgstr "Herstellen" msgstr "Herstellen"
#: common/views.py:683
msgid "Format de fond de plan invalide"
msgstr "Ongeldig basiskaartformaat"

View file

@ -381,7 +381,8 @@
// Extract or build basemaps list // Extract or build basemaps list
this.basemaps = this._resolveBasemaps(); this.basemaps = this._resolveBasemaps();
this.activeBasemapId = localStorage.getItem('loko_active_basemap') || 'default'; this.activeBasemapId = options.initialBasemapId || localStorage.getItem('loko_active_basemap') || 'default';
this.onBasemapChange = typeof options.onBasemapChange === 'function' ? options.onBasemapChange : null;
this._injectStyles(); this._injectStyles();
this._handleOutsideClick = this._handleOutsideClick.bind(this); this._handleOutsideClick = this._handleOutsideClick.bind(this);
@ -442,13 +443,26 @@
this.popover = this._createPopover(); this.popover = this._createPopover();
map.getContainer().appendChild(this.popover); map.getContainer().appendChild(this.popover);
// Initialize active basemap on map load // Initialize active basemap on map load (checking map._loaded, isStyleLoaded, or loaded)
if (map.loaded()) { const isMapReady = Boolean(
map._loaded ||
(typeof map.isStyleLoaded === 'function' && map.isStyleLoaded()) ||
map.loaded()
);
if (isMapReady) {
this._applyInitialBasemap(); this._applyInitialBasemap();
} else { } else {
map.on('load', () => this._applyInitialBasemap()); map.once('load', () => this._applyInitialBasemap());
} }
// Re-apply if the style reloads dynamically
map.on('style.load', () => {
if (this.activeBasemapId && this.activeBasemapId !== 'default') {
this._applyInitialBasemap();
}
});
document.addEventListener('click', this._handleOutsideClick); document.addEventListener('click', this._handleOutsideClick);
return this.container; return this.container;
@ -660,8 +674,11 @@
} }
selectBasemap(id) { selectBasemap(id) {
const previousId = this.activeBasemapId;
this.activeBasemapId = id; this.activeBasemapId = id;
try {
localStorage.setItem('loko_active_basemap', id); localStorage.setItem('loko_active_basemap', id);
} catch (e) {}
// Update UI list items // Update UI list items
if (this.popover) { if (this.popover) {
@ -682,9 +699,20 @@
this._applyRasterBasemap(bgConfig); this._applyRasterBasemap(bgConfig);
} }
} }
if (this.onBasemapChange && previousId !== id) {
try {
this.onBasemapChange(id);
} catch (err) {
console.error('[LayerSwitcherControl] onBasemapChange error:', err);
}
}
} }
_applyInitialBasemap() { _applyInitialBasemap() {
if (!this.basemaps || this.basemaps.length === 0) {
this.basemaps = this._resolveBasemaps();
}
if (this.activeBasemapId && this.activeBasemapId !== 'default') { if (this.activeBasemapId && this.activeBasemapId !== 'default') {
const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId); const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId);
if (bgConfig) { if (bgConfig) {
@ -693,14 +721,16 @@
return; return;
} }
} }
// Check if any basemap has is_default // Check if any basemap has is_default only if activeBasemapId is not set
if (!this.activeBasemapId) {
const defaultBg = this.basemaps.find(b => b.is_default); const defaultBg = this.basemaps.find(b => b.is_default);
if (defaultBg) { if (defaultBg) {
this.selectBasemap(defaultBg.id); this.selectBasemap(defaultBg.id);
} else { return;
this._updateButtonState();
} }
} }
this._updateButtonState();
}
_removeCurrentRasterLayer() { _removeCurrentRasterLayer() {
if (!this.map) return; if (!this.map) return;

View file

@ -324,6 +324,35 @@ class AssetsIndexPreferencesTests(TestCase):
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIn("error", response.json()) self.assertIn("error", response.json())
def test_save_assets_index_preferences_with_basemap(self):
self.client.force_login(self.superuser)
save_url = reverse("common:save_assets_index_preferences")
payload = {
"thematics": ["sign"],
"basemap": "urbis_ortho",
}
response = self.client.post(save_url, json.dumps(payload), content_type="application/json")
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json().get("success"))
from common.models import UserConfig
config = UserConfig.objects.get(user=self.superuser)
self.assertEqual(
config.assets_index_preferences.get("basemap"),
"urbis_ortho"
)
def test_save_assets_index_preferences_invalid_basemap_returns_400(self):
self.client.force_login(self.superuser)
save_url = reverse("common:save_assets_index_preferences")
payload = {
"basemap": 9999,
}
response = self.client.post(save_url, json.dumps(payload), content_type="application/json")
self.assertEqual(response.status_code, 400)
self.assertIn("error", response.json())
class UserConfigCopyTests(TestCase): class UserConfigCopyTests(TestCase):

View file

@ -663,6 +663,7 @@ def save_assets_index_preferences(request):
include_no_geom = data.get('include_no_geom', True) include_no_geom = data.get('include_no_geom', True)
map_center = data.get('map_center') map_center = data.get('map_center')
map_zoom = data.get('map_zoom') map_zoom = data.get('map_zoom')
basemap = data.get('basemap')
if not isinstance(thematics, list): if not isinstance(thematics, list):
return JsonResponse({'error': _('Format de thématiques invalide')}, status=400) return JsonResponse({'error': _('Format de thématiques invalide')}, status=400)
@ -680,13 +681,18 @@ def save_assets_index_preferences(request):
return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400) return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400)
if not isinstance(validation_status_filters, list): if not isinstance(validation_status_filters, list):
return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400) return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400)
if basemap is not None and not isinstance(basemap, str):
return JsonResponse({'error': _('Format de fond de plan invalide')}, status=400)
try: try:
user_config, _created = UserConfig.objects.get_or_create(user=request.user) user_config, _created = UserConfig.objects.get_or_create(user=request.user)
existing_prefs = user_config.assets_index_preferences or {} existing_prefs = user_config.assets_index_preferences or {}
existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else [] if not isinstance(existing_prefs, dict):
existing_prefs = {}
existing_visible_columns = existing_prefs.get('visible_columns', [])
if not isinstance(existing_visible_columns, list): if not isinstance(existing_visible_columns, list):
existing_visible_columns = [] existing_visible_columns = []
existing_basemap = existing_prefs.get('basemap', 'default')
user_config.assets_index_preferences = { user_config.assets_index_preferences = {
'thematics': thematics, 'thematics': thematics,
@ -700,6 +706,7 @@ def save_assets_index_preferences(request):
'include_no_geom': include_no_geom, 'include_no_geom': include_no_geom,
'map_center': map_center, 'map_center': map_center,
'map_zoom': map_zoom, 'map_zoom': map_zoom,
'basemap': basemap if basemap is not None else existing_basemap,
} }
user_config.save(update_fields=['assets_index_preferences']) user_config.save(update_fields=['assets_index_preferences'])
return JsonResponse({'success': True}) return JsonResponse({'success': True})