Skip to content

Commit 884ed3a

Browse files
committed
Re: #1606 Add TreeLayerControl to Folium
1 parent 309aa08 commit 884ed3a

File tree

7 files changed

+324
-1
lines changed

7 files changed

+324
-1
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dist/
77
docs/index.html
88
docs/_build/
99
docs/quickstart.ipynb
10+
docs/**/*.ipynb
1011
examples/results/*
1112
.cache/
1213
.idea/

Diff for: docs/user_guide/plugins/realtime.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import folium
66
import folium.plugins
77
```
88

9-
# Realtime plugin
9+
# Realtime
1010

1111
Put realtime data on a Leaflet map: live tracking GPS units,
1212
sensor data or just about anything.

Diff for: docs/user_guide/plugins/treelayercontrol.md

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
```{code-cell} ipython3
2+
---
3+
nbsphinx: hidden
4+
---
5+
import folium
6+
import folium.plugins
7+
```
8+
9+
# TreeLayerControl
10+
Create a Layer Control allowing a tree structure for the layers.
11+
12+
See https://github.com/jjimenezshaw/Leaflet.Control.Layers.Tree for more
13+
information.
14+
15+
## Simple example
16+
17+
```{code-cell} ipython3
18+
import folium
19+
from folium.plugins.treelayercontrol import TreeLayerControl
20+
from folium.features import Marker
21+
22+
m = folium.Map(location=[46.603354, 1.8883335], zoom_start=5)
23+
osm = folium.TileLayer("openstreetmap").add_to(m)
24+
25+
overlay_tree = {
26+
"label": "Points of Interest",
27+
"select_all_checkbox": "Un/select all",
28+
"children": [
29+
{
30+
"label": "Europe",
31+
"select_all_checkbox": True,
32+
"children": [
33+
{
34+
"label": "France",
35+
"select_all_checkbox": True,
36+
"children": [
37+
{ "label": "Tour Eiffel", "layer": Marker([48.8582441, 2.2944775]).add_to(m) },
38+
{ "label": "Notre Dame", "layer": Marker([48.8529540, 2.3498726]).add_to(m) },
39+
{ "label": "Louvre", "layer": Marker([48.8605847, 2.3376267]).add_to(m) },
40+
]
41+
}, {
42+
"label": "Germany",
43+
"select_all_checkbox": True,
44+
"children": [
45+
{ "label": "Branderburger Tor", "layer": Marker([52.5162542, 13.3776805]).add_to(m)},
46+
{ "label": "Kölner Dom", "layer": Marker([50.9413240, 6.9581201]).add_to(m)},
47+
]
48+
}, {"label": "Spain",
49+
"select_all_checkbox": "De/seleccionar todo",
50+
"children": [
51+
{ "label": "Palacio Real", "layer": Marker([40.4184145, -3.7137051]).add_to(m)},
52+
{ "label": "La Alhambra", "layer": Marker([37.1767829, -3.5892795]).add_to(m)},
53+
]
54+
}
55+
]
56+
}, {
57+
"label": "Asia",
58+
"select_all_checkbox": True,
59+
"children": [
60+
{
61+
"label": "Jordan",
62+
"select_all_checkbox": True,
63+
"children": [
64+
{ "label": "Petra", "layer": Marker([30.3292215, 35.4432464]).add_to(m) },
65+
{ "label": "Wadi Rum", "layer": Marker([29.6233486, 35.4390656]).add_to(m) }
66+
]
67+
}, {
68+
}
69+
]
70+
}
71+
]
72+
}
73+
74+
control = TreeLayerControl(overlay_tree=overlay_tree).add_to(m)
75+
```

Diff for: folium/plugins/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from folium.plugins.time_slider_choropleth import TimeSliderChoropleth
3232
from folium.plugins.timestamped_geo_json import TimestampedGeoJson
3333
from folium.plugins.timestamped_wmstilelayer import TimestampedWmsTileLayers
34+
from folium.plugins.treelayercontrol import TreeLayerControl
3435
from folium.plugins.vectorgrid_protobuf import VectorGridProtobuf
3536

3637
__all__ = [
@@ -66,5 +67,6 @@
6667
"TimeSliderChoropleth",
6768
"TimestampedGeoJson",
6869
"TimestampedWmsTileLayers",
70+
"TreeLayerControl",
6971
"VectorGridProtobuf",
7072
]

Diff for: folium/plugins/treelayercontrol.py

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from typing import Union
2+
3+
from branca.element import MacroElement
4+
5+
from folium.elements import JSCSSMixin
6+
from folium.template import Template
7+
from folium.utilities import parse_options
8+
9+
10+
class TreeLayerControl(JSCSSMixin, MacroElement):
11+
"""
12+
Create a Layer Control allowing a tree structure for the layers.
13+
See https://github.com/jjimenezshaw/Leaflet.Control.Layers.Tree for more
14+
information.
15+
16+
Parameters
17+
----------
18+
base_tree : dict
19+
A dictionary defining the base layers.
20+
Valid elements are
21+
22+
children: list
23+
Array of child nodes for this node. Nothing special.
24+
label: str
25+
Text displayed in the tree for this node. It may contain HTML code.
26+
layer: Layer
27+
The layer itself. This needs to be added to the map.
28+
name: str
29+
Text displayed in the toggle when control is minimized.
30+
If not present, label is used. It makes sense only when
31+
namedToggle is true, and with base layers.
32+
radioGroup: str, default ''
33+
Text to identify different radio button groups.
34+
It is used in the name attribute in the radio button.
35+
It is used only in the overlays layers (ignored in the base
36+
layers), allowing you to have radio buttons instead of checkboxes.
37+
See that radio groups cannot be unselected, so create a 'fake'
38+
layer (like L.layersGroup([])) if you want to disable it.
39+
Default '' (that means checkbox).
40+
collapsed: bool, default False
41+
Indicate whether this tree node should be collapsed initially,
42+
useful for opening large trees partially based on user input or
43+
context.
44+
selectAllCheckbox: bool or str
45+
Displays a checkbox to select/unselect all overlays in the
46+
sub-tree. In case of being a <str>, that text will be the title
47+
(tooltip). When any overlay in the sub-tree is clicked, the
48+
checkbox goes into indeterminate state (a dash in the box).
49+
overlay_tree: dict
50+
Similar to baseTree, but for overlays.
51+
closed_symbol: str, default '+',
52+
Symbol displayed on a closed node (that you can click to open).
53+
opened_symbol: str, default '-',
54+
Symbol displayed on an opened node (that you can click to close).
55+
space_symbol: str, default ' ',
56+
Symbol between the closed or opened symbol, and the text.
57+
selector_back: bool, default False,
58+
Flag to indicate if the selector (+ or −) is after the text.
59+
named_toggle: bool, default False,
60+
Flag to replace the toggle image (box with the layers image) with the
61+
'name' of the selected base layer. If the name field is not present in
62+
the tree for this layer, label is used. See that you can show a
63+
different name when control is collapsed than the one that appears
64+
in the tree when it is expanded.
65+
collapse_all: str, default '',
66+
Text for an entry in control that collapses the tree (baselayers or
67+
overlays). If empty, no entry is created.
68+
expand_all: str, default '',
69+
Text for an entry in control that expands the tree. If empty, no entry
70+
is created
71+
label_is_selector: str, default 'both',
72+
Controls if a label or only the checkbox/radiobutton can toggle layers.
73+
If set to `both`, `overlay` or `base` those labels can be clicked
74+
on to toggle the layer.
75+
**kwargs
76+
Additional (possibly inherited) options. See
77+
https://leafletjs.com/reference.html#control-layers
78+
79+
Examples
80+
--------
81+
>>> import folium
82+
>>> from folium.plugins.treelayercontrol import TreeLayerControl
83+
>>> from folium.features import Marker
84+
85+
>>> m = folium.Map(location=[46.603354, 1.8883335], zoom_start=5)
86+
>>> osm = folium.TileLayer("openstreetmap").add_to(m)
87+
88+
>>> marker = Marker([48.8582441, 2.2944775]).add_to(m)
89+
90+
>>> overlay_tree = {
91+
... "label": "Points of Interest",
92+
... "selectAllCheckbox": "Un/select all",
93+
... "children": [
94+
... {
95+
... "label": "Europe",
96+
... "selectAllCheckbox": True,
97+
... "children": [
98+
... {
99+
... "label": "France",
100+
... "selectAllCheckbox": True,
101+
... "children": [
102+
... {"label": "Tour Eiffel", "layer": marker},
103+
... ],
104+
... }
105+
... ],
106+
... }
107+
... ],
108+
... }
109+
110+
>>> control = TreeLayerControl(overlay_tree=overlay_tree).add_to(m)
111+
"""
112+
113+
default_js = [
114+
(
115+
"L.Control.Layers.Tree.min.js",
116+
"https://cdn.jsdelivr.net/npm/leaflet.control.layers.tree@1.1.0/L.Control.Layers.Tree.min.js", # noqa
117+
),
118+
]
119+
default_css = [
120+
(
121+
"L.Control.Layers.Tree.min.css",
122+
"https://cdn.jsdelivr.net/npm/leaflet.control.layers.tree@1.1.0/L.Control.Layers.Tree.min.css", # noqa
123+
)
124+
]
125+
126+
_template = Template(
127+
"""
128+
{% macro script(this,kwargs) %}
129+
L.control.layers.tree(
130+
{{this.base_tree|tojavascript}},
131+
{{this.overlay_tree|tojavascript}},
132+
{{this.options|tojson}}
133+
).addTo({{this._parent.get_name()}});
134+
{% endmacro %}
135+
"""
136+
)
137+
138+
def __init__(
139+
self,
140+
base_tree: Union[dict, list, None] = None,
141+
overlay_tree: Union[dict, list, None] = None,
142+
closed_symbol: str = "+",
143+
opened_symbol: str = "-",
144+
space_symbol: str = "&nbsp;",
145+
selector_back: bool = False,
146+
named_toggle: bool = False,
147+
collapse_all: str = "",
148+
expand_all: str = "",
149+
label_is_selector: str = "both",
150+
**kwargs
151+
):
152+
super().__init__()
153+
self._name = "TreeLayerControl"
154+
kwargs["closed_symbol"] = closed_symbol
155+
kwargs["openened_symbol"] = opened_symbol
156+
kwargs["space_symbol"] = space_symbol
157+
kwargs["selector_back"] = selector_back
158+
kwargs["named_toggle"] = named_toggle
159+
kwargs["collapse_all"] = collapse_all
160+
kwargs["expand_all"] = expand_all
161+
kwargs["label_is_selector"] = label_is_selector
162+
self.options = parse_options(**kwargs)
163+
self.base_tree = base_tree
164+
self.overlay_tree = overlay_tree

Diff for: folium/template.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json
2+
from typing import Union
3+
4+
import jinja2
5+
from branca.element import Element
6+
7+
from folium.utilities import JsCode, TypeJsonValue, camelize
8+
9+
10+
def tojavascript(obj: Union[str, JsCode, dict, list]) -> str:
11+
if isinstance(obj, JsCode):
12+
return obj.js_code
13+
elif isinstance(obj, Element):
14+
return obj.get_name()
15+
elif isinstance(obj, dict):
16+
out = ["{\n"]
17+
for key, value in obj.items():
18+
out.append(f' "{camelize(key)}": ')
19+
out.append(tojavascript(value))
20+
out.append(",\n")
21+
out.append("}")
22+
return "".join(out)
23+
elif isinstance(obj, list):
24+
out = ["[\n"]
25+
for value in obj:
26+
out.append(tojavascript(value))
27+
out.append(",\n")
28+
out.append("]")
29+
return "".join(out)
30+
else:
31+
return _to_escaped_json(obj)
32+
33+
34+
def _to_escaped_json(obj: TypeJsonValue) -> str:
35+
return (
36+
json.dumps(obj)
37+
.replace("<", "\\u003c")
38+
.replace(">", "\\u003e")
39+
.replace("&", "\\u0026")
40+
.replace("'", "\\u0027")
41+
)
42+
43+
44+
class Environment(jinja2.Environment):
45+
def __init__(self, *args, **kwargs):
46+
super().__init__(*args, **kwargs)
47+
self.filters["tojavascript"] = tojavascript
48+
49+
50+
class Template(jinja2.Template):
51+
environment_class = Environment

Diff for: tests/test_template.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import folium
2+
from folium.template import tojavascript
3+
from folium.utilities import JsCode
4+
5+
6+
def test_tojavascript():
7+
trail_coordinates = [
8+
(-71.351871840295871, -73.655963711222626),
9+
(-71.374144382613707, -73.719861619751498),
10+
(-71.391042575973145, -73.784922248007007),
11+
(-71.400964450973134, -73.851042243124397),
12+
(-71.402411391077322, -74.050048183880477),
13+
]
14+
15+
trail = folium.PolyLine(trail_coordinates, tooltip="Coast")
16+
d = {
17+
"label": "Base Layers",
18+
"children": [
19+
{
20+
"label": "World &#x1f5fa;",
21+
"children": [
22+
{"label": "trail", "layer": trail},
23+
{"jscode": JsCode('function(){return "hi"}')},
24+
],
25+
}
26+
],
27+
}
28+
js = tojavascript(d)
29+
assert "poly_line" in js
30+
assert 'return "hi"' in js

0 commit comments

Comments
 (0)