Skip to content

Commit

Permalink
Re: python-visualization#1606 Add TreeLayerControl to Folium
Browse files Browse the repository at this point in the history
  • Loading branch information
hansthen committed Mar 8, 2024
1 parent 309aa08 commit 884ed3a
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dist/
docs/index.html
docs/_build/
docs/quickstart.ipynb
docs/**/*.ipynb
examples/results/*
.cache/
.idea/
Expand Down
2 changes: 1 addition & 1 deletion docs/user_guide/plugins/realtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import folium
import folium.plugins
```

# Realtime plugin
# Realtime

Put realtime data on a Leaflet map: live tracking GPS units,
sensor data or just about anything.
Expand Down
75 changes: 75 additions & 0 deletions docs/user_guide/plugins/treelayercontrol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
```{code-cell} ipython3
---
nbsphinx: hidden
---
import folium
import folium.plugins
```

# TreeLayerControl
Create a Layer Control allowing a tree structure for the layers.

See https://github.com/jjimenezshaw/Leaflet.Control.Layers.Tree for more
information.

## Simple example

```{code-cell} ipython3
import folium
from folium.plugins.treelayercontrol import TreeLayerControl
from folium.features import Marker
m = folium.Map(location=[46.603354, 1.8883335], zoom_start=5)
osm = folium.TileLayer("openstreetmap").add_to(m)
overlay_tree = {
"label": "Points of Interest",
"select_all_checkbox": "Un/select all",
"children": [
{
"label": "Europe",
"select_all_checkbox": True,
"children": [
{
"label": "France",
"select_all_checkbox": True,
"children": [
{ "label": "Tour Eiffel", "layer": Marker([48.8582441, 2.2944775]).add_to(m) },
{ "label": "Notre Dame", "layer": Marker([48.8529540, 2.3498726]).add_to(m) },
{ "label": "Louvre", "layer": Marker([48.8605847, 2.3376267]).add_to(m) },
]
}, {
"label": "Germany",
"select_all_checkbox": True,
"children": [
{ "label": "Branderburger Tor", "layer": Marker([52.5162542, 13.3776805]).add_to(m)},
{ "label": "Kölner Dom", "layer": Marker([50.9413240, 6.9581201]).add_to(m)},
]
}, {"label": "Spain",
"select_all_checkbox": "De/seleccionar todo",
"children": [
{ "label": "Palacio Real", "layer": Marker([40.4184145, -3.7137051]).add_to(m)},
{ "label": "La Alhambra", "layer": Marker([37.1767829, -3.5892795]).add_to(m)},
]
}
]
}, {
"label": "Asia",
"select_all_checkbox": True,
"children": [
{
"label": "Jordan",
"select_all_checkbox": True,
"children": [
{ "label": "Petra", "layer": Marker([30.3292215, 35.4432464]).add_to(m) },
{ "label": "Wadi Rum", "layer": Marker([29.6233486, 35.4390656]).add_to(m) }
]
}, {
}
]
}
]
}
control = TreeLayerControl(overlay_tree=overlay_tree).add_to(m)
```
2 changes: 2 additions & 0 deletions folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from folium.plugins.time_slider_choropleth import TimeSliderChoropleth
from folium.plugins.timestamped_geo_json import TimestampedGeoJson
from folium.plugins.timestamped_wmstilelayer import TimestampedWmsTileLayers
from folium.plugins.treelayercontrol import TreeLayerControl
from folium.plugins.vectorgrid_protobuf import VectorGridProtobuf

__all__ = [
Expand Down Expand Up @@ -66,5 +67,6 @@
"TimeSliderChoropleth",
"TimestampedGeoJson",
"TimestampedWmsTileLayers",
"TreeLayerControl",
"VectorGridProtobuf",
]
164 changes: 164 additions & 0 deletions folium/plugins/treelayercontrol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from typing import Union

from branca.element import MacroElement

from folium.elements import JSCSSMixin
from folium.template import Template
from folium.utilities import parse_options


class TreeLayerControl(JSCSSMixin, MacroElement):
"""
Create a Layer Control allowing a tree structure for the layers.
See https://github.com/jjimenezshaw/Leaflet.Control.Layers.Tree for more
information.
Parameters
----------
base_tree : dict
A dictionary defining the base layers.
Valid elements are
children: list
Array of child nodes for this node. Nothing special.
label: str
Text displayed in the tree for this node. It may contain HTML code.
layer: Layer
The layer itself. This needs to be added to the map.
name: str
Text displayed in the toggle when control is minimized.
If not present, label is used. It makes sense only when
namedToggle is true, and with base layers.
radioGroup: str, default ''
Text to identify different radio button groups.
It is used in the name attribute in the radio button.
It is used only in the overlays layers (ignored in the base
layers), allowing you to have radio buttons instead of checkboxes.
See that radio groups cannot be unselected, so create a 'fake'
layer (like L.layersGroup([])) if you want to disable it.
Default '' (that means checkbox).
collapsed: bool, default False
Indicate whether this tree node should be collapsed initially,
useful for opening large trees partially based on user input or
context.
selectAllCheckbox: bool or str
Displays a checkbox to select/unselect all overlays in the
sub-tree. In case of being a <str>, that text will be the title
(tooltip). When any overlay in the sub-tree is clicked, the
checkbox goes into indeterminate state (a dash in the box).
overlay_tree: dict
Similar to baseTree, but for overlays.
closed_symbol: str, default '+',
Symbol displayed on a closed node (that you can click to open).
opened_symbol: str, default '-',
Symbol displayed on an opened node (that you can click to close).
space_symbol: str, default ' ',
Symbol between the closed or opened symbol, and the text.
selector_back: bool, default False,
Flag to indicate if the selector (+ or −) is after the text.
named_toggle: bool, default False,
Flag to replace the toggle image (box with the layers image) with the
'name' of the selected base layer. If the name field is not present in
the tree for this layer, label is used. See that you can show a
different name when control is collapsed than the one that appears
in the tree when it is expanded.
collapse_all: str, default '',
Text for an entry in control that collapses the tree (baselayers or
overlays). If empty, no entry is created.
expand_all: str, default '',
Text for an entry in control that expands the tree. If empty, no entry
is created
label_is_selector: str, default 'both',
Controls if a label or only the checkbox/radiobutton can toggle layers.
If set to `both`, `overlay` or `base` those labels can be clicked
on to toggle the layer.
**kwargs
Additional (possibly inherited) options. See
https://leafletjs.com/reference.html#control-layers
Examples
--------
>>> import folium
>>> from folium.plugins.treelayercontrol import TreeLayerControl
>>> from folium.features import Marker
>>> m = folium.Map(location=[46.603354, 1.8883335], zoom_start=5)
>>> osm = folium.TileLayer("openstreetmap").add_to(m)
>>> marker = Marker([48.8582441, 2.2944775]).add_to(m)
>>> overlay_tree = {
... "label": "Points of Interest",
... "selectAllCheckbox": "Un/select all",
... "children": [
... {
... "label": "Europe",
... "selectAllCheckbox": True,
... "children": [
... {
... "label": "France",
... "selectAllCheckbox": True,
... "children": [
... {"label": "Tour Eiffel", "layer": marker},
... ],
... }
... ],
... }
... ],
... }
>>> control = TreeLayerControl(overlay_tree=overlay_tree).add_to(m)
"""

default_js = [
(
"L.Control.Layers.Tree.min.js",
"https://cdn.jsdelivr.net/npm/[email protected]/L.Control.Layers.Tree.min.js", # noqa
),
]
default_css = [
(
"L.Control.Layers.Tree.min.css",
"https://cdn.jsdelivr.net/npm/[email protected]/L.Control.Layers.Tree.min.css", # noqa
)
]

_template = Template(
"""
{% macro script(this,kwargs) %}
L.control.layers.tree(
{{this.base_tree|tojavascript}},
{{this.overlay_tree|tojavascript}},
{{this.options|tojson}}
).addTo({{this._parent.get_name()}});
{% endmacro %}
"""
)

def __init__(
self,
base_tree: Union[dict, list, None] = None,
overlay_tree: Union[dict, list, None] = None,
closed_symbol: str = "+",
opened_symbol: str = "-",
space_symbol: str = "&nbsp;",
selector_back: bool = False,
named_toggle: bool = False,
collapse_all: str = "",
expand_all: str = "",
label_is_selector: str = "both",
**kwargs
):
super().__init__()
self._name = "TreeLayerControl"
kwargs["closed_symbol"] = closed_symbol
kwargs["openened_symbol"] = opened_symbol
kwargs["space_symbol"] = space_symbol
kwargs["selector_back"] = selector_back
kwargs["named_toggle"] = named_toggle
kwargs["collapse_all"] = collapse_all
kwargs["expand_all"] = expand_all
kwargs["label_is_selector"] = label_is_selector
self.options = parse_options(**kwargs)
self.base_tree = base_tree
self.overlay_tree = overlay_tree
51 changes: 51 additions & 0 deletions folium/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import json
from typing import Union

import jinja2
from branca.element import Element

from folium.utilities import JsCode, TypeJsonValue, camelize


def tojavascript(obj: Union[str, JsCode, dict, list]) -> str:
if isinstance(obj, JsCode):
return obj.js_code
elif isinstance(obj, Element):
return obj.get_name()
elif isinstance(obj, dict):
out = ["{\n"]
for key, value in obj.items():
out.append(f' "{camelize(key)}": ')
out.append(tojavascript(value))
out.append(",\n")
out.append("}")
return "".join(out)
elif isinstance(obj, list):
out = ["[\n"]
for value in obj:
out.append(tojavascript(value))
out.append(",\n")
out.append("]")
return "".join(out)
else:
return _to_escaped_json(obj)


def _to_escaped_json(obj: TypeJsonValue) -> str:
return (
json.dumps(obj)
.replace("<", "\\u003c")
.replace(">", "\\u003e")
.replace("&", "\\u0026")
.replace("'", "\\u0027")
)


class Environment(jinja2.Environment):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["tojavascript"] = tojavascript


class Template(jinja2.Template):
environment_class = Environment
30 changes: 30 additions & 0 deletions tests/test_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import folium
from folium.template import tojavascript
from folium.utilities import JsCode


def test_tojavascript():
trail_coordinates = [
(-71.351871840295871, -73.655963711222626),
(-71.374144382613707, -73.719861619751498),
(-71.391042575973145, -73.784922248007007),
(-71.400964450973134, -73.851042243124397),
(-71.402411391077322, -74.050048183880477),
]

trail = folium.PolyLine(trail_coordinates, tooltip="Coast")
d = {
"label": "Base Layers",
"children": [
{
"label": "World &#x1f5fa;",
"children": [
{"label": "trail", "layer": trail},
{"jscode": JsCode('function(){return "hi"}')},
],
}
],
}
js = tojavascript(d)
assert "poly_line" in js
assert 'return "hi"' in js

0 comments on commit 884ed3a

Please sign in to comment.