Skip to content

Commit 5c46067

Browse files
authored
Issue 1359 (#2159)
* Fix for issue 1359 This includes a structural change. I added a Class object with a include method. This follows leaflet's `L.Class.include` statement. This allows users to override Leaflet class behavior. The motivating example for this can be found in the added `test_include` in `test_map.py`. Using an include, users can override the `createTile` method of `L.TileLayer` and add a headers. * Close #1359 Add an include statement, that will allow users to override specific methods at Leaflet level. This allows users to customize the "createTile" method using a JsCode object. * Add documentation
1 parent 2de8ff2 commit 5c46067

File tree

6 files changed

+189
-8
lines changed

6 files changed

+189
-8
lines changed

docs/advanced_guide.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Advanced guide
1515
advanced_guide/piechart_icons
1616
advanced_guide/polygons_from_list_of_points
1717
advanced_guide/customize_javascript_and_css
18+
advanced_guide/override_leaflet_class_methods
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Overriding Leaflet class methods
2+
3+
```{code-cell} ipython3
4+
---
5+
nbsphinx: hidden
6+
---
7+
import folium
8+
```
9+
10+
## Customizing Leaflet behavior
11+
Sometimes you want to override Leaflet's javascript behavior. This can be done using the `Class.include` statement. This mimics Leaflet's
12+
`L.Class.include` method. See [here](https://leafletjs.com/examples/extending/extending-1-classes.html) for more details.
13+
14+
### Example: adding an authentication header to a TileLayer
15+
One such use case is if you need to override the `createTile` on `L.TileLayer`, because your tiles are hosted on an oauth2 protected
16+
server. This can be done like this:
17+
18+
```{code-cell}
19+
create_tile = folium.JsCode("""
20+
function(coords, done) {
21+
const url = this.getTileUrl(coords);
22+
const img = document.createElement('img');
23+
fetch(url, {
24+
headers: {
25+
"Authorization": "Bearer <Token>"
26+
},
27+
})
28+
.then((response) => {
29+
img.src = URL.createObjectURL(response.body);
30+
done(null, img);
31+
})
32+
return img;
33+
}
34+
""")
35+
36+
folium.TileLayer.include(create_tile=create_tile)
37+
tiles = folium.TileLayer(
38+
tiles="OpenStreetMap",
39+
)
40+
m = folium.Map(
41+
tiles=tiles,
42+
)
43+
44+
45+
m = folium.Map()
46+
```

folium/elements.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,26 @@ def __init__(self, element_name: str, element_parent_name: str):
159159
self.element_parent_name = element_parent_name
160160

161161

162+
class IncludeStatement(MacroElement):
163+
"""Generate an include statement on a class."""
164+
165+
_template = Template(
166+
"""
167+
{{ this.leaflet_class_name }}.include(
168+
{{ this.options | tojavascript }}
169+
)
170+
"""
171+
)
172+
173+
def __init__(self, leaflet_class_name: str, **kwargs):
174+
super().__init__()
175+
self.leaflet_class_name = leaflet_class_name
176+
self.options = kwargs
177+
178+
def render(self, *args, **kwargs):
179+
return super().render(*args, **kwargs)
180+
181+
162182
class MethodCall(MacroElement):
163183
"""Abstract class to add an element to another element."""
164184

folium/features.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
from folium.elements import JSCSSMixin
3838
from folium.folium import Map
39-
from folium.map import FeatureGroup, Icon, Layer, Marker, Popup, Tooltip
39+
from folium.map import Class, FeatureGroup, Icon, Layer, Marker, Popup, Tooltip
4040
from folium.template import Template
4141
from folium.utilities import (
4242
JsCode,
@@ -2023,7 +2023,7 @@ def __init__(
20232023
self.add_child(PolyLine(val, color=key, weight=weight, opacity=opacity))
20242024

20252025

2026-
class Control(JSCSSMixin, MacroElement):
2026+
class Control(JSCSSMixin, Class):
20272027
"""
20282028
Add a Leaflet Control object to the map
20292029

folium/map.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
"""
55

66
import warnings
7-
from collections import OrderedDict
8-
from typing import TYPE_CHECKING, Optional, Sequence, Union, cast
7+
from collections import OrderedDict, defaultdict
8+
from typing import TYPE_CHECKING, DefaultDict, Optional, Sequence, Union, cast
99

1010
from branca.element import Element, Figure, Html, MacroElement
1111

12-
from folium.elements import ElementAddToElement, EventHandler
12+
from folium.elements import ElementAddToElement, EventHandler, IncludeStatement
1313
from folium.template import Template
1414
from folium.utilities import (
1515
JsCode,
@@ -22,11 +22,58 @@
2222
validate_location,
2323
)
2424

25+
26+
class classproperty:
27+
def __init__(self, f):
28+
self.f = f
29+
30+
def __get__(self, obj, owner):
31+
return self.f(owner)
32+
33+
2534
if TYPE_CHECKING:
2635
from folium.features import CustomIcon, DivIcon
2736

2837

29-
class Evented(MacroElement):
38+
class Class(MacroElement):
39+
"""The root class of the leaflet class hierarchy"""
40+
41+
_includes: DefaultDict[str, dict] = defaultdict(dict)
42+
43+
@classmethod
44+
def include(cls, **kwargs):
45+
cls._includes[cls].update(**kwargs)
46+
47+
@classproperty
48+
def includes(cls):
49+
return cls._includes[cls]
50+
51+
@property
52+
def leaflet_class_name(self):
53+
# TODO: I did not check all Folium classes to see if
54+
# this holds up. This breaks at least for CustomIcon.
55+
return f"L.{self._name}"
56+
57+
def render(self, **kwargs):
58+
figure = self.get_root()
59+
assert isinstance(
60+
figure, Figure
61+
), "You cannot render this Element if it is not in a Figure."
62+
if self.includes:
63+
stmt = IncludeStatement(self.leaflet_class_name, **self.includes)
64+
# A bit weird. I tried adding IncludeStatement directly to both
65+
# figure and script, but failed. So we render this ourself.
66+
figure.script.add_child(
67+
Element(stmt._template.render(this=stmt, kwargs=self.includes)),
68+
# make sure each class include gets rendered only once
69+
name=self._name + "_includes",
70+
# make sure this renders before the element itself
71+
index=-1,
72+
)
73+
super().render(**kwargs)
74+
75+
76+
class Evented(Class):
3077
"""The base class for Layer and Map
3178
3279
Adds the `on` and `once` methods for event handling capabilities.

tests/test_map.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import pytest
1111

1212
from folium import GeoJson, Map, TileLayer
13-
from folium.map import CustomPane, Icon, LayerControl, Marker, Popup
14-
from folium.utilities import normalize
13+
from folium.map import Class, CustomPane, Icon, LayerControl, Marker, Popup
14+
from folium.utilities import JsCode, normalize
1515

1616
tmpl = """
1717
<div id="{id}"
@@ -148,6 +148,73 @@ def test_popup_show():
148148
assert normalize(rendered) == normalize(expected)
149149

150150

151+
def test_include():
152+
create_tile = """
153+
function(coords, done) {
154+
const url = this.getTileUrl(coords);
155+
const img = document.createElement('img');
156+
fetch(url, {
157+
headers: {
158+
"Authorization": "Bearer <Token>"
159+
},
160+
})
161+
.then((response) => {
162+
img.src = URL.createObjectURL(response.body);
163+
done(null, img);
164+
})
165+
return img;
166+
}
167+
"""
168+
TileLayer.include(create_tile=JsCode(create_tile))
169+
tiles = TileLayer(
170+
tiles="OpenStreetMap",
171+
)
172+
m = Map(
173+
tiles=tiles,
174+
)
175+
rendered = m.get_root().render()
176+
Class._includes.clear()
177+
expected = """
178+
L.TileLayer.include({
179+
"createTile":
180+
function(coords, done) {
181+
const url = this.getTileUrl(coords);
182+
const img = document.createElement('img');
183+
fetch(url, {
184+
headers: {
185+
"Authorization": "Bearer <Token>"
186+
},
187+
})
188+
.then((response) => {
189+
img.src = URL.createObjectURL(response.body);
190+
done(null, img);
191+
})
192+
return img;
193+
},
194+
})
195+
"""
196+
assert normalize(expected) in normalize(rendered)
197+
198+
199+
def test_include_once():
200+
abc = "MY BEAUTIFUL SENTINEL"
201+
TileLayer.include(abc=abc)
202+
tiles = TileLayer(
203+
tiles="OpenStreetMap",
204+
)
205+
m = Map(
206+
tiles=tiles,
207+
)
208+
TileLayer(
209+
tiles="OpenStreetMap",
210+
).add_to(m)
211+
212+
rendered = m.get_root().render()
213+
Class._includes.clear()
214+
215+
assert rendered.count(abc) == 1, "Includes should happen only once per class"
216+
217+
151218
def test_popup_backticks():
152219
m = Map()
153220
popup = Popup("back`tick`tick").add_to(m)

0 commit comments

Comments
 (0)