Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add groupedlayercontrol plugin #1592

Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ miniconda.sh
latest_logs/
*.nbconvert.ipynb
folium/_version.py
.venv
1 change: 1 addition & 0 deletions folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from folium.plugins.float_image import FloatImage
from folium.plugins.fullscreen import Fullscreen
from folium.plugins.geocoder import Geocoder
from folium.plugins.groupedlayercontrol import GroupedLayerControl
chansooligans marked this conversation as resolved.
Show resolved Hide resolved
from folium.plugins.heat_map import HeatMap
from folium.plugins.heat_map_withtime import HeatMapWithTime
from folium.plugins.locate_control import LocateControl
Expand Down
147 changes: 147 additions & 0 deletions folium/plugins/groupedlayercontrol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from collections import OrderedDict

from folium.elements import JSCSSMixin
from folium.map import Layer, LayerControl
from folium.utilities import parse_options

from jinja2 import Template


class GroupedLayerControl(JSCSSMixin, LayerControl):
"""
Creates a GroupedLayerControl object to be added on a folium map.
Allows grouping overlays together so that within groups, overlays are
mutually exclusive (radio buttons).

Parameters
----------
groups : dict
A dictionary where the keys are group names and the values are overlay names to be
displayed with radio buttons. Overlays NOT specified in this dictionary are
added with check boxes.
position : str
The position of the control (one of the map corners), can be
'topleft', 'topright', 'bottomleft' or 'bottomright'
default: 'topright'
collapsed : bool, default True
If true the control will be collapsed into an icon and expanded on
mouse hover or touch.
autoZIndex : bool, default True
If true the control assigns zIndexes in increasing order to all of
its layers so that the order is preserved when switching them on/off.
**kwargs
Additional (possibly inherited) options. See
https://leafletjs.com/reference-1.6.0.html#control-layers

"""
default_js = [
('leaflet.groupedlayercontrol.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.js'),
('leaflet.groupedlayercontrol.min.js.map',
'https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.js.map')
chansooligans marked this conversation as resolved.
Show resolved Hide resolved
]
default_css = [
('leaflet.groupedlayercontrol.min.css',
'https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.css')
]

_template = Template("""
{% macro script(this,kwargs) %}
var {{ this.get_name() }} = {
base_layers : {
{%- for key, val in this.base_layers.items() %}
{{ key|tojson }} : {{val}},
{%- endfor %}
},
overlays : {
{%- for key, val in this.un_grouped_overlays.items() %}
{{ key|tojson }} : {{val}},
{%- endfor %}
},
};

L.control.layers(
{{ this.get_name() }}.base_layers,
{{ this.get_name() }}.overlays,
{{ this.options|tojson }}
).addTo({{this._parent.get_name()}});

var groupedOverlays = {
{%- for key, overlays in this.grouped_overlays.items() %}
{{ key|tojson }} : {
{%- for overlaykey, val in overlays.items() %}
{{ overlaykey|tojson }} : {{val}},
{%- endfor %}
},
{%- endfor %}
};

var options = {
exclusiveGroups: [
{%- for key, value in this.grouped_overlays.items() %}
{{key|tojson}},
{%- endfor %}
],
collapsed: {{ this.options["collapsed"]|tojson }},
autoZIndex: {{ this.options["autoZIndex"]|tojson }},
position: {{ this.options["position"]|tojson }},
};

L.control.groupedLayers(
null,
groupedOverlays,
options,
).addTo({{this._parent.get_name()}});

{%- for val in this.layers_untoggle.values() %}
{{ val }}.remove();
{%- endfor %}

{% endmacro %}
""")

def __init__(
self,
groups,
position='topright',
collapsed=False,
autoZIndex=True,
**kwargs
):
super(GroupedLayerControl, self).__init__()
self._name = 'GroupedLayerControl'
self.groups = {x: key for key, sublist in groups.items() for x in sublist}
self.options = parse_options(
position=position,
collapsed=collapsed,
autoZIndex=autoZIndex,
**kwargs
)
self.base_layers = OrderedDict()
self.un_grouped_overlays = OrderedDict()
self.layers_untoggle = OrderedDict()
self.grouped_overlays = OrderedDict()
for val in self.groups.values():
self.grouped_overlays[val] = OrderedDict()

def render(self, **kwargs):
"""Renders the HTML representation of the element."""
for item in self._parent._children.values():
if not isinstance(item, Layer) or not item.control:
continue
key = item.layer_name

if not item.overlay:
self.base_layers[key] = item.get_name()
if len(self.base_layers) > 1:
self.layers_untoggle[key] = item.get_name()
else:
if key in self.groups.keys():
self.grouped_overlays[self.groups[key]][key] = item.get_name()
if not item.show:
self.layers_untoggle[key] = item.get_name()
else:
self.un_grouped_overlays[key] = item.get_name()
if not item.show:
self.layers_untoggle[key] = item.get_name()
super(GroupedLayerControl, self).render()
85 changes: 85 additions & 0 deletions tests/plugins/test_grouped_layer_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
Test GroupedLayerControl
------------------
"""

import folium
from folium.plugins import groupedlayercontrol
from folium.utilities import normalize

from jinja2 import Template

# %%
def test_feature_group_sub_group():
chansooligans marked this conversation as resolved.
Show resolved Hide resolved
m = folium.Map([40., 70.], zoom_start=6)
fg1 = folium.FeatureGroup(name='g1')
fg2 = folium.FeatureGroup(name='g2')
fg3 = folium.FeatureGroup(name='g3')
folium.Marker([40, 74]).add_to(fg1)
folium.Marker([38, 72]).add_to(fg2)
folium.Marker([40, 72]).add_to(fg3)
m.add_child(fg1)
m.add_child(fg2)
m.add_child(fg3)
lc = groupedlayercontrol.GroupedLayerControl(groups={'groups1':['g1','g2']})
chansooligans marked this conversation as resolved.
Show resolved Hide resolved
lc.add_to(m)
out = normalize(m._parent.render())

# We verify that imports
assert '<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.js"></script>' in out
chansooligans marked this conversation as resolved.
Show resolved Hide resolved
assert '<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.js.map"></script>' in out
chansooligans marked this conversation as resolved.
Show resolved Hide resolved
assert '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.min.css"/>' in out
chansooligans marked this conversation as resolved.
Show resolved Hide resolved

# Verify the script part is okay.
tmpl = Template("""
var {{ this.get_name() }} = {
base_layers : {
{%- for key, val in this.base_layers.items() %}
{{ key|tojson }} : {{val}},
{%- endfor %}
},
overlays : {
{%- for key, val in this.un_grouped_overlays.items() %}
{{ key|tojson }} : {{val}},
{%- endfor %}
},
};

L.control.layers(
{{ this.get_name() }}.base_layers,
{{ this.get_name() }}.overlays,
{{ this.options|tojson }}
).addTo({{this._parent.get_name()}});

var groupedOverlays = {
{%- for key, overlays in this.grouped_overlays.items() %}
{{ key|tojson }} : {
{%- for overlaykey, val in overlays.items() %}
{{ overlaykey|tojson }} : {{val}},
{%- endfor %}
},
{%- endfor %}
};

var options = {
exclusiveGroups: [
{%- for key, value in this.grouped_overlays.items() %}
{{key|tojson}},
{%- endfor %}
],
collapsed: {{ this.options["collapsed"]|tojson }},
autoZIndex: {{ this.options["autoZIndex"]|tojson }},
position: {{ this.options["position"]|tojson }},
};

L.control.groupedLayers(
null,
groupedOverlays,
options,
).addTo({{this._parent.get_name()}});

{%- for val in this.layers_untoggle.values() %}
{{ val }}.remove();
{%- endfor %}
""")
assert normalize(tmpl.render(this=lc)) in out