Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions folium/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ def __init__(self, element_name: str, element_parent_name: str):
self.element_parent_name = element_parent_name


class IncludeStatement(MacroElement):
"""Generate an include statement on a class."""

_template = Template(
"""
{{ this.leaflet_class_name }}.include(
{{ this.options | tojavascript }}
)
"""
)

def __init__(self, leaflet_class_name: str, **kwargs):
super().__init__()
self.leaflet_class_name = leaflet_class_name
self.options = kwargs

def render(self, *args, **kwargs):
return super().render(*args, **kwargs)

class MethodCall(MacroElement):
"""Abstract class to add an element to another element."""

Expand Down
4 changes: 2 additions & 2 deletions folium/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

from folium.elements import JSCSSMixin
from folium.folium import Map
from folium.map import FeatureGroup, Icon, Layer, Marker, Popup, Tooltip
from folium.map import Class, FeatureGroup, Icon, Layer, Marker, Popup, Tooltip
from folium.template import Template
from folium.utilities import (
JsCode,
Expand Down Expand Up @@ -2023,7 +2023,7 @@ def __init__(
self.add_child(PolyLine(val, color=key, weight=weight, opacity=opacity))


class Control(JSCSSMixin, MacroElement):
class Control(JSCSSMixin, Class):
"""
Add a Leaflet Control object to the map

Expand Down
55 changes: 51 additions & 4 deletions folium/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"""

import warnings
from collections import OrderedDict
from typing import TYPE_CHECKING, Optional, Sequence, Union, cast
from collections import OrderedDict, defaultdict
from typing import TYPE_CHECKING, DefaultDict, Optional, Sequence, Union, cast

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

from folium.elements import ElementAddToElement, EventHandler
from folium.elements import ElementAddToElement, EventHandler, IncludeStatement
from folium.template import Template
from folium.utilities import (
JsCode,
Expand All @@ -22,11 +22,58 @@
validate_location,
)


class classproperty:
def __init__(self, f):
self.f = f

def __get__(self, obj, owner):
return self.f(owner)


if TYPE_CHECKING:
from folium.features import CustomIcon, DivIcon


class Evented(MacroElement):
class Class(MacroElement):
"""The root class of the leaflet class hierarchy"""

_includes: DefaultDict[str, dict] = defaultdict(dict)

@classmethod
def include(cls, **kwargs):
cls._includes[cls].update(**kwargs)

@classproperty
def includes(cls):
return cls._includes[cls]

@property
def leaflet_class_name(self):
# TODO: I did not check all Folium classes to see if
# this holds up. This breaks at least for CustomIcon.
return f"L.{self._name}"

def render(self, **kwargs):
figure = self.get_root()
assert isinstance(
figure, Figure
), "You cannot render this Element if it is not in a Figure."
if self.includes:
stmt = IncludeStatement(self.leaflet_class_name, **self.includes)
# A bit weird. I tried adding IncludeStatement directly to both
# figure and script, but failed. So we render this ourself.
figure.script.add_child(
Element(stmt._template.render(this=stmt, kwargs=self.includes)),
# make sure each class include gets rendered only once
name=self._name + "_includes",
# make sure this renders before the element itself
index=-1,
)
super().render(**kwargs)


class Evented(Class):
"""The base class for Layer and Map

Adds the `on` and `once` methods for event handling capabilities.
Expand Down
75 changes: 73 additions & 2 deletions tests/test_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import pytest

from folium import GeoJson, Map, TileLayer
from folium.map import CustomPane, Icon, LayerControl, Marker, Popup
from folium.utilities import normalize
from folium.map import Class, CustomPane, Icon, LayerControl, Marker, Popup
from folium.utilities import JsCode, normalize

tmpl = """
<div id="{id}"
Expand Down Expand Up @@ -148,6 +148,77 @@ def test_popup_show():
assert normalize(rendered) == normalize(expected)


def test_include():
create_tile = """
function(coords, done) {
const url = this.getTileUrl(coords);
const img = document.createElement('img');
fetch(url, {
headers: {
"Authorization": "Bearer <Token>"
},
})
.then((response) => {
img.src = URL.createObjectURL(response.body);
done(null, img);
})
return img;
}
"""
TileLayer.include(create_tile=JsCode(create_tile))
tiles = TileLayer(
tiles="OpenStreetMap",
)
m = Map(
tiles=tiles,
)
rendered = m.get_root().render()
Class._includes.clear()
expected = """
L.TileLayer.include({
"createTile":
function(coords, done) {
const url = this.getTileUrl(coords);
const img = document.createElement('img');
fetch(url, {
headers: {
"Authorization": "Bearer <Token>"
},
})
.then((response) => {
img.src = URL.createObjectURL(response.body);
done(null, img);
})
return img;
},
})
"""
print(expected)
print("-----")
print(rendered)

assert normalize(expected) in normalize(rendered)


def test_include_once():
abc = "MY BEAUTIFUL SENTINEL"
TileLayer.include(abc=abc)
tiles = TileLayer(
tiles="OpenStreetMap",
)
m = Map(
tiles=tiles,
)
TileLayer(
tiles="OpenStreetMap",
).add_to(m)

rendered = m.get_root().render()
Class._includes.clear()

assert rendered.count(abc) == 1, "Includes should happen only once per class"


def test_popup_backticks():
m = Map()
popup = Popup("back`tick`tick").add_to(m)
Expand Down
Loading