From e65c40239567535c8a4569389cb5c53af35f7740 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 9 Oct 2026 14:16:12 +0200 Subject: [PATCH] feat: add basemap preference persistence and layer switcher control support --- loko/assets/static/assets/unified_index.js | 22 +++++++- .../templates/assets/unified_index.html | 4 +- loko/common/locale/en/LC_MESSAGES/django.mo | Bin 7530 -> 7601 bytes loko/common/locale/en/LC_MESSAGES/django.po | 5 ++ loko/common/locale/fr/LC_MESSAGES/django.mo | Bin 9409 -> 9513 bytes loko/common/locale/fr/LC_MESSAGES/django.po | 5 ++ loko/common/locale/nl/LC_MESSAGES/django.mo | Bin 7020 -> 7095 bytes loko/common/locale/nl/LC_MESSAGES/django.po | 5 ++ .../static/common/layer-switcher-control.js | 52 ++++++++++++++---- loko/common/tests.py | 29 ++++++++++ loko/common/views.py | 9 ++- 11 files changed, 116 insertions(+), 15 deletions(-) diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js index c2b8ade..2218f18 100644 --- a/loko/assets/static/assets/unified_index.js +++ b/loko/assets/static/assets/unified_index.js @@ -748,6 +748,14 @@ function applyInitialPreferences() { window._restoredMapCenter = prefs.map_center; 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) { // First visit: pre-select the user's default thematic activeThematics = new Set([DEFAULT_THEMATIC_CODE]); @@ -775,6 +783,9 @@ function savePreferences() { ).map(cb => cb.value); const includeNoGeomCheckbox = document.getElementById('include-no-geom'); const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true; + const activeBasemap = (window.unifiedLayerSwitcher && window.unifiedLayerSwitcher.activeBasemapId) + || localStorage.getItem('loko_active_basemap') + || 'default'; const prefs = { thematics: Array.from(activeThematics), categories: Array.from(activeCategories), @@ -787,6 +798,7 @@ function savePreferences() { include_no_geom: includeNoGeom, map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null, map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null, + basemap: activeBasemap, }; fetch(SAVE_ASSETS_PREFS_URL, { method: 'POST', @@ -2929,7 +2941,12 @@ maploader.addEventListener('mapLoaded', () => { // Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost) if (typeof LayerSwitcherControl !== 'undefined') { + const initialBasemap = window._restoredBasemap || localStorage.getItem('loko_active_basemap') || 'default'; const layerSwitcher = new LayerSwitcherControl({ + initialBasemapId: initialBasemap, + onBasemapChange: function (basemapId) { + schedulePreferencesSave(); + }, layers: [{ id: 'basemap', name: 'Fonds de plan & Orthophotos', @@ -2937,7 +2954,10 @@ maploader.addEventListener('mapLoaded', () => { beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'), 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({ onAdd(m) { this._map = m; diff --git a/loko/assets/templates/assets/unified_index.html b/loko/assets/templates/assets/unified_index.html index e7dea0c..ebe2085 100644 --- a/loko/assets/templates/assets/unified_index.html +++ b/loko/assets/templates/assets/unified_index.html @@ -1315,7 +1315,7 @@ {% block script %} - + - + {% endblock script %} \ No newline at end of file diff --git a/loko/common/locale/en/LC_MESSAGES/django.mo b/loko/common/locale/en/LC_MESSAGES/django.mo index 7eb40fc4940dc2501d283165dfeb535f43375bb8..a64342fe6a887eb15f1adbb2773fa5c7fe93f934 100644 GIT binary patch delta 2216 zcmXxlUrd%&9Ki9T$X}88rw9V^$3%zGM3*6;qNWrS38|$Np@7N&nYOy?-5NQXQ>%Bm z)kTfwTGOdhR^HU6T~!;aG|G##w5>I3YdNb6SoZzG!x_KlbDr})&pGFJ&Us&MuQ^{E zyOBNT?Lg@z3W>gi5N!A~i3=q&HH1pc!g*MaIoO05*n!13fGKzs7vc#_#&fs`FQWZi z#d5rdc_G9?dD?V`wOGvuE$Be6VgbH^%kXsMI68rQ=mc`+hLDS8=s>ISer&@mJcOC} z8rtuPsQn}^Vf^q3l?8lo9hc%=WD!D9dI%3bS@iuY zNLS%|T!**t0W4z{Cf0~aj34$;F{9qdSFnovx6q}%jAr&TF2>2Iy_EFrXgxlJyRa0G zUeI{@D~^NdKT-e z1?U-AiTPNEKDQm0U=O+_N74S?&t(1QQ~8_*2fl_b<&Rj2zhV_;^Oh58Ks# z@F1E<96h(EkYvMobOM*q_r5_7TnKj!xr3$ z9?DZ_Cf}h0|BL*E|G1by7W?XR_2`!D#SODq*{J|H_?tK(236FfVegJX!~MxfK_NhThT+@jc)M}dWeUS=VRdv6$ktb&Fm|5pl{L4 ze@8RDi*}I7GPo7F=-J3e6KF)g4Fi~nucK$+IGXu;==0~0uEGtR@Av;7D$X#SQSG1- zS*@@c-LrNyqapOU5hVF=3H@F80e$`twBy-yV&(;Crd8;~+t9OEN1+0fQm03 zLudS9bpJ8h@fe!%FX$HihA#aCI-yjS#|)REhp`5oKs!3{AacILi|B+7qhI&a7<2E& zs4)3(4PEj(Sb`I1riG;M1XiL;`w05nTJ*gw(dV7$7WPD+_oJD0s}<^r=LiqZ zOxZ@o^=6XcDPk94@fU2zRBig=VX7vcCtT(p!ct50&P>_$wR90Hh)0Pv@$1QX`5UNN z%84ygwGf`f*7&@XivFjmt&Li@q6cUD=K;2?CYs_SDV0U-)ZE->iN}adgk>Gk5x<$7Gdh`aH8H*Cz(7|?S9i%^U*|x) aAtQJ6>x?hcl6Q9=>hA9x939TTmGD1((8Zbn delta 2175 zcmXxkTWnNC7{Kv?mTn8%(hJZQT4+I9>T*#c2t2D1;ZqOAS#WD1n%es8Rp_-C?qGezSA-%*^-AoE>UA zvn=yNW#RLIvV~el?Jo$yjsrzpEF~eNU<{VxJgmYtEW>p;9ec1Cw_^_H=Y1$}-5 z87dsXG#UPBM#2k1N_=z@<{kbn2|1RbvEHzd2^Uo^rbHw$q#uEWdG!?+EN zq&;bfSG|d~d<$0KINXSyfnGH70rdA7{SWGk{OVJAAkessV?XvD|S2!BNvoMccBT{#+Y4LZ&o^tsho#{8i-b_}2c z52BI2gl^3~H01};0Y=csM$tof0bRgk(&fZ0$oUQP&;_N@*SsIyvO#2xVHajh^*b~g z@F22{;RL#XvuH}sqrba|K4@CJKNa1&8R-2cG|~m=!qc&TeeCZ>pL+<`;-ht;D|g5~ zq{GNhqA9z8M)((c2q)C%BTu2<*C20G?qBaJ+)Q<0K4Wq4joI&urnR)w8cQWbx$*$l zHtJ2(-ds=qa!s}|QC)Il?72I;GEtSelW*qRVjeB)^W;`ZHo|i1T~rUurP4vee6z^h zpGH@_;Xl_Mu`M2^h15+{(|jM*vW&Xr(vr=urH^_g^;+uE?C*){+Lg2{v#8hSS|Qww zw`J>#n;+gl>+0BhD|&$LpJ8LYRLcr#PxgGGYB*VPeBAJVWuK29{;~F0!T&+WxaR-> diff --git a/loko/common/locale/en/LC_MESSAGES/django.po b/loko/common/locale/en/LC_MESSAGES/django.po index cdbbd5c..5c1f710 100644 --- a/loko/common/locale/en/LC_MESSAGES/django.po +++ b/loko/common/locale/en/LC_MESSAGES/django.po @@ -553,3 +553,8 @@ msgstr "Zoom in" #: common/templates/common/components/document_viewer.html msgid "Réinitialiser" msgstr "Reset" + +#: common/views.py:683 +msgid "Format de fond de plan invalide" +msgstr "Invalid basemap format" + diff --git a/loko/common/locale/fr/LC_MESSAGES/django.mo b/loko/common/locale/fr/LC_MESSAGES/django.mo index 8d040b29ad2b38b3801bb40dab477e9af05d57d1..94e12aeb87588fbb027abf3e44f922fdb31fd5b6 100644 GIT binary patch delta 2655 zcmZwHeQ1?c9Ki82b-L-ax#pZY^Xhz?y3MT@Yn9v7S#y@TwUt^no6hCSomtaAj4oIx z3&FxHBS;~oayUc(F_B;qmXxAct{?(g5kY381-&4Bzq`)`|LAak&*!<%z4x5o`JHoa zPwfYb65r zH|FAJXqVsNT>JsYg^&n^!;%A3Vj&-_$I-YOi}5{lps%qUe?}82$O>UB&cGt9iEKx| z{}!6q5j25Q$bjJj`ui(5f$_sX+>GSGIG$!>G5UqMTztM5YjGvk;rqB0uj3S4$S6~B z1Lol#oQ{W)Vup)YjJ;?g>69-A$6^-ahhlE3umUIGHq6Ag(TW^GJ350*KAgo${1#1M z3|U)=N*s*K(F&}J?7$MDtj8EX8`9h}&@*9z+xT67Ap@ zj9~`T@p%zifm-x;&1mA=(F*QBUg10!mOu36P=D9@R~}5l!K7`+Mabkr4Z4XMBjf1i z+KFTERrGs@(Iq&ENbKUq85}@2$5AxX zbI2?7a$&OJH*`knOwaQ>99^o#=m2f#W_=x9(i7;eKaUP{1Isao4Q3)W$SV;VxSDx9 zav%DGPte3Jp$Ytm4){0PK^|}1bd%9FUx4i6(1d=zHhSKHwY=Ynb$9`n;&|3?is!$b z8#m8hq-x<5dVVjWo9171CSiOskrK4@^O3)f(1cakf+lnjGx1ZjBInS4e#c?>CsyJB z$~}eg!#r-7RA@uj_9e6euSC9sp5J50D_rKnB>G45_!cm#&#TdXnz0nOp~tWb-4kb# zSNMaAzaJH&{zNF_;s^C;1vaBU>_ju~LQ8lQo%vtrY02VCuJveibC#kV*Q1*(j_#os zBi}^#)`!S;2p>(P{(kWy4_tyibS;O{n;jN#;Q)k6G?7NMw9C*<*Me4NH=5ALXvb&J zQukpDuc8ynW<9L%G-OpnNns+n$?AAuBFoVZ)}g!oWpuz@Xy$LA71)m^b`0HYpQAIq zgdX3kxCF0b5iVez$RWhh1YSe?+2{YSGwDLV*o&@J8h`9*Xj`15|M?p3A0n!WdBkG` zMe85T-Vs1S=StiBzkLZag6#(>sZU&flGQH{k}!CYBSnr-@laE#W_wEuN%*Qx1ym zH)16*m$;W$O*~Gt63-I0Cx{tDxo>Fq5YG@r3%IN!Zf^5=(?+;42Sy)c9KVv;GbF3E eJ-#}&dQEK8hE?%BJ=wX(yGLe@_`jz^)Bgj82jfft delta 2588 zcmZA1Z)nw37{Kwzxy}Dt)7)~-U2Wbum#(I1x^m80Zt7CArkR<4Qp=TpruibLHwvdw zltI0a5GgD)F-eC=q~!~XN_=AxgauMW34ycT=oC>#)R2i9XDuEz_i887g=W6#l&HBMQ70G_B?u2xh&TN5@?amN6&OKTJ3w$iB4fPevKy5jn>!zE+n3cWTq1f z(I32kX7)OozyWl^6X*aJ(W1MIp84NM-Ns;U^7~Ts^9k69wKxx7!A1BT`qY$jj!B>Y zCOYg^Y(t;hSJ9&BKv!}aP2^W}>${M@iWppyd2EZ(gqCAJY(saX9UbRGw5C47TKoh} z;3ke>{z$O@o^1`f1GBO=qgA>AdBuJ%Bp;{I$M)Oo=Qof|h$6D6!b#|3xCX6>-RSR* zVFJHGUU8L6%9Z!$Q-3fT&A1-jx;f~I-$b91L+BYFM~m`Hbl|IKapf}zi^{(DyM@qq(&AcY>^qlx^1ZfPf0Vi&q2<=kvS&FH|Z&@JA93EYD& z>@d2spCP9fU!b-1Gn&ZtF{#V|IUJZ(J`$a<49$EZx&u?u#O9(!wh~?GM)a}WgA4FA ztia329FfmCnLq_P&Ryt2>e2f)`Ty$~?Z;l;K-lgl9MI+fE7HA8Qg!sHopgJa>q4S7 z^EUEud$#{Dwh(KGx!KR=qlwiMRYYnNmq!TZPgnU;e4dy=xK+;(;|W*&|K?-oBgLu2 zJ;ZFnwuP8PH28+LmS{@%G8b#6ny~)TpSS}yw|EKRS=t^VJm;qgE8NFs72(!V^s$w& zemn{v)t=Va)4@5!vh4fm_yqA7F^-t*{Lj&`^&>VC(})KM+uek9;n_74+lXAkRzoaL z_x`&)neFDFg}Iq{oUj<5BIXf2ZHS%B`ZHDC%KItAgMLAyhSHD8Vzhew)3JEg5w-;J zDB*vGkL3p9UcxquSWnC(Y*gDLzu#k(dSQM3r=Gb zW-gk2uN7Bto=5sChTSwS=fWOr$Ai&R=s?rxK>y$ZOxJ|aj5)j-`>`HJ@DiLr6MZUP z-;YZ;e;XU|8(fNItYrL+sD_M3`x3zqONv~(3LBz=c!q{A3GfhVyFU&BrKI=17lXrlG?)ZYQSN#BkNXof@RgL}}Dzl#GEtZlqs z$HJOe0UdZFaw{b0(oCX>me93+3*9p((9)kl_u9E4jV>A)&YW=pE%64-;wbv!KIGAG z44vUgG~v_e^X2Hdc)x+HOel+1Y6Y^LLIK@tgXkV9K19O-oVFA*QOKQl$(%S zVGLXG8T5zhC|c^T(XZt!I)DrB^Nr|0UD5Su;t87Q3z+u%f0>3ed=u#-97kXL3N2w7 z9pI06zm}U;q6N7WI?<(Di&@-)R_-x$m%oH2^eH;<4`}6n#6^rB$}}ut1*7;iw4$5x zTAYWy@p?Zx@L+T^`qgZY=ey8NIT0ygsIwU_K_4g}Ur1Pk zSsX@k43DEFd*LuLydd<>epc6Ta(BG3 z65U{a1wDkHo5ziW8_?f}q_i;AyJ~>e+_8>Efw17K34eCxj*T?#BG%4bz&nYXO0T9m za<|axCR}pT2{#ZPu7LY-d+C={bNjut{3W`T@MtGSh;4+M#~pkJv1%$;y=`84*Tnc} mc64X<;oZr2>7C4isV6e)YHIFJCLfp_OOi((o%*2ZRONr2IjI8x delta 2005 zcmX}sUuczO9Ki8kZtma7=4QI(DLt#Lc0A2crZd-En>y2UHg#EMn&>A`q0qJ6DuAC~p*`0EA315`PvWOgJaTfOC9Ndjh;m5cDk76mFLEE3h7jO(~a1woP zZh6E~%p(2eV<(mQT-b|QJd$_;eP9e7=srG;6PU)Tiim~SgjKi+XJbD)(f!HwL--=U zPvJZq#YX%Aix@v1QfcDCKj;A2R75qd!Y8o@9jFf(I}V{MJBB1H&LW=}=BpjApvjv+ z7f?heT7!!*hh}UCrWrp5snp|8a^o_ZkuiJ)|H3+~XIU>{7n*?q%-|R31iwXBcpYs& zj!v+c^s%-mL$_iMK94!f+fg@_O5BUJ_(@^`*|s=~&)`M0-H&KWZ=L){8mp$HjOYozUe<@=uoHdoGx=n@FE=3;D!dzC2V9(T@H_CsfWxyG7OLfDOoE zqZM7idMw5daWn40ERLcRy@QTdTt)uvD8(|I;R3Yb1~kF@w$MgS|+T7(`ci5}ojA zwEg+SYsvj_bV3i%OwC}H9G^&`XRH=IBW-B^`E69(tApr_KSiF6)966MXh+|ndwv65 z;h$&+lj!qx^y*eM;0kO?et(2Ci2-!NhtZXP_4s!_ZX^wUNB3p|J(LT{7u9IR2Hb-F zK^;U>J%o0A5p8!J9bi0h0-f+Y4ulhILo?BVF031CeE&b7;)8=|>I&%VcV?6@!T9kL6;I=DI1}%A1OGq= zo=hxZR=zEj==C&u80!<8(dX8n6Wolp>%(r`gPws)=!Ad9ybT^u;f;t#n86D2&zO-z zQ@9p+31SDaiD)6}2#YJTtS9WNmvFE*2v`3a;WnB2SBdwD9>NK(_Wyq!m44zm!s6ku zG!iSOY7uWGI&0=QH8~I4Hp2HiLs(vZ%>Uob)Vh*iTX6>A+q8&ykC=MNSAhr1)4z=H zHM6u49z6dSb``RvEgc=yrpsC?D+v?cN%(g&T{cj8hgdUx0k;$Fg|AC 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); return this.container; @@ -660,8 +674,11 @@ } selectBasemap(id) { + const previousId = this.activeBasemapId; this.activeBasemapId = id; - localStorage.setItem('loko_active_basemap', id); + try { + localStorage.setItem('loko_active_basemap', id); + } catch (e) {} // Update UI list items if (this.popover) { @@ -682,9 +699,20 @@ this._applyRasterBasemap(bgConfig); } } + + if (this.onBasemapChange && previousId !== id) { + try { + this.onBasemapChange(id); + } catch (err) { + console.error('[LayerSwitcherControl] onBasemapChange error:', err); + } + } } _applyInitialBasemap() { + if (!this.basemaps || this.basemaps.length === 0) { + this.basemaps = this._resolveBasemaps(); + } if (this.activeBasemapId && this.activeBasemapId !== 'default') { const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId); if (bgConfig) { @@ -693,13 +721,15 @@ return; } } - // Check if any basemap has is_default - const defaultBg = this.basemaps.find(b => b.is_default); - if (defaultBg) { - this.selectBasemap(defaultBg.id); - } else { - this._updateButtonState(); + // 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); + if (defaultBg) { + this.selectBasemap(defaultBg.id); + return; + } } + this._updateButtonState(); } _removeCurrentRasterLayer() { diff --git a/loko/common/tests.py b/loko/common/tests.py index f0ab794..48fbb8e 100644 --- a/loko/common/tests.py +++ b/loko/common/tests.py @@ -324,6 +324,35 @@ class AssetsIndexPreferencesTests(TestCase): self.assertEqual(response.status_code, 400) 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): diff --git a/loko/common/views.py b/loko/common/views.py index 1683737..c5c7cec 100644 --- a/loko/common/views.py +++ b/loko/common/views.py @@ -663,6 +663,7 @@ def save_assets_index_preferences(request): include_no_geom = data.get('include_no_geom', True) map_center = data.get('map_center') map_zoom = data.get('map_zoom') + basemap = data.get('basemap') if not isinstance(thematics, list): 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) if not isinstance(validation_status_filters, list): 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: user_config, _created = UserConfig.objects.get_or_create(user=request.user) 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): existing_visible_columns = [] + existing_basemap = existing_prefs.get('basemap', 'default') user_config.assets_index_preferences = { 'thematics': thematics, @@ -700,6 +706,7 @@ def save_assets_index_preferences(request): 'include_no_geom': include_no_geom, 'map_center': map_center, 'map_zoom': map_zoom, + 'basemap': basemap if basemap is not None else existing_basemap, } user_config.save(update_fields=['assets_index_preferences']) return JsonResponse({'success': True})