From 2a3ec27301ba291eea3217a566fb41666165b5fd Mon Sep 17 00:00:00 2001 From: DresdenGman Date: Wed, 4 Nov 2026 12:43:45 +0000 Subject: [PATCH] Add use_local_storage option to LayerControl Save the user's layer control choices (the active base layer and the visible overlays) to the browser's local storage and restore them when the page is loaded again, so the choices survive a refresh. Opt-in, defaults to off and renders nothing extra when disabled. Fixes #1636 --- folium/map.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_map.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/folium/map.py b/folium/map.py index daf7ee84cb..4494423a91 100644 --- a/folium/map.py +++ b/folium/map.py @@ -264,6 +264,11 @@ class LayerControl(MacroElement): draggable: bool, default False By default the layer control has a fixed position. Set this argument to True to allow dragging the control around. + use_local_storage: bool, default False + If true, the user's layer control choices (the active base layer + and the visible overlays) are saved to the browser's local storage + and restored when the page is loaded again, for example after a + refresh. The choices are stored per page, keyed by the page path. **kwargs Additional (possibly inherited) options. See https://leafletjs.com/reference.html#control-layers @@ -294,6 +299,61 @@ class LayerControl(MacroElement): new L.Draggable({{ this.get_name() }}.getContainer()).enable(); {%- endif %} + {%- if this.use_local_storage %} + (function () { + var layer_control_map = {{ this._parent.get_name() }}; + var layer_control_layers = {{ this.get_name() }}_layers; + var layer_control_storage_key = "folium-layer-control:" + window.location.pathname; + function save_layer_control_state() { + var state = {base: null, overlays: []}; + for (var name in layer_control_layers.base_layers) { + if (layer_control_map.hasLayer(layer_control_layers.base_layers[name])) { + state.base = name; + } + } + for (var name in layer_control_layers.overlays) { + if (layer_control_map.hasLayer(layer_control_layers.overlays[name])) { + state.overlays.push(name); + } + } + try { + localStorage.setItem(layer_control_storage_key, JSON.stringify(state)); + } catch (e) {} + } + function restore_layer_control_state() { + var state = null; + try { + state = JSON.parse(localStorage.getItem(layer_control_storage_key)); + } catch (e) {} + if (!state) { + return; + } + for (var name in layer_control_layers.base_layers) { + var layer = layer_control_layers.base_layers[name]; + if (name === state.base) { + if (!layer_control_map.hasLayer(layer)) { + layer_control_map.addLayer(layer); + } + } else if (layer_control_map.hasLayer(layer)) { + layer_control_map.removeLayer(layer); + } + } + for (var name in layer_control_layers.overlays) { + var layer = layer_control_layers.overlays[name]; + if (state.overlays.indexOf(name) !== -1) { + if (!layer_control_map.hasLayer(layer)) { + layer_control_map.addLayer(layer); + } + } else if (layer_control_map.hasLayer(layer)) { + layer_control_map.removeLayer(layer); + } + } + } + restore_layer_control_state(); + layer_control_map.on("baselayerchange overlayadd overlayremove", save_layer_control_state); + })(); + {%- endif %} + {% endmacro %} """) @@ -303,6 +363,7 @@ def __init__( collapsed: bool = True, autoZIndex: bool = True, draggable: bool = False, + use_local_storage: bool = False, **kwargs: TypeJsonValue, ): super().__init__() @@ -311,6 +372,7 @@ def __init__( position=position, collapsed=collapsed, autoZIndex=autoZIndex, **kwargs ) self.draggable = draggable + self.use_local_storage = use_local_storage self.base_layers: OrderedDict[str, str] = OrderedDict() self.overlays: OrderedDict[str, str] = OrderedDict() diff --git a/tests/test_map.py b/tests/test_map.py index 594c42ce0e..9fc785fdfd 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -70,6 +70,43 @@ def test_layer_control_draggable(): assert normalize(expected) in normalize(rendered) +def test_layer_control_use_local_storage(): + m = Map(tiles=None) + TileLayer("openstreetmap").add_to(m) + TileLayer("cartodbpositron").add_to(m) + layer_control = LayerControl(use_local_storage=True).add_to(m) + + assert layer_control.use_local_storage is True + # the flag is a Python-side option, it must not leak into the + # options passed to L.control.layers + assert "use_local_storage" not in layer_control.options + + rendered = m.get_root().render() + # state is saved when the user changes layers + assert "localStorage.setItem" in rendered + assert "localStorage.getItem" in rendered + assert '"folium-layer-control:" + window.location.pathname' in rendered + assert f"var layer_control_map = {m.get_name()};" in rendered + expected = ( + 'layer_control_map.on("baselayerchange overlayadd overlayremove", ' + "save_layer_control_state);" + ) + assert normalize(expected) in normalize(rendered) + # and restored on load + assert "restore_layer_control_state();" in rendered + + +def test_layer_control_use_local_storage_default_off(): + m = Map(tiles=None) + TileLayer("openstreetmap").add_to(m) + layer_control = LayerControl().add_to(m) + + assert layer_control.use_local_storage is False + + rendered = m.get_root().render() + assert "localStorage" not in rendered + + def test_popup_ascii(): popup = Popup("Some text.") _id = list(popup.html._children.keys())[0]