Skip to content

Commit

Permalink
process JS code separately
Browse files Browse the repository at this point in the history
  • Loading branch information
Conengmo committed Jan 3, 2024
1 parent 4e6ebfe commit 2725ffe
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 76 deletions.
2 changes: 1 addition & 1 deletion docs/user_guide/plugins/mouse_position.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ m
```{code-cell} ipython3
m = folium.Map()
formatter = "function(num) {return L.Util.formatNum(num, 3) + ' ° ';};"
formatter = "function(num) {return L.Util.formatNum(num, 3) + ' ° '}"
MousePosition(
position="topright",
Expand Down
29 changes: 9 additions & 20 deletions folium/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from typing import Dict, List, Optional, Sequence, Tuple, Type, Union

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

from folium.elements import ElementAddToElement
from folium.template import Template
from folium.utilities import (
JsCode,
TypeBounds,
TypeJsonValue,
camelize,
Expand Down Expand Up @@ -108,7 +109,7 @@ def __init__(
super().__init__(name=name, overlay=overlay, control=control, show=show)
self._name = "FeatureGroup"
self.tile_name = name if name is not None else self.get_name()
self.options = parse_options(**kwargs)
self.options = kwargs


class LayerControl(MacroElement):
Expand Down Expand Up @@ -147,21 +148,9 @@ class LayerControl(MacroElement):
_template = Template(
"""
{% macro script(this,kwargs) %}
var {{ this.get_name() }}_layers = {
base_layers : {
{%- for key, val in this.base_layers.items() %}
{{ key|tojson }} : {{val}},
{%- endfor %}
},
overlays : {
{%- for key, val in this.overlays.items() %}
{{ key|tojson }} : {{val}},
{%- endfor %}
},
};
let {{ this.get_name() }} = L.control.layers(
{{ this.get_name() }}_layers.base_layers,
{{ this.get_name() }}_layers.overlays,
{{ this.base_layers|tojavascript }},
{{ this.overlays|tojavascript }},
{{ this.options|tojson }}
).addTo({{this._parent.get_name()}});
Expand All @@ -187,8 +176,8 @@ def __init__(
position=position, collapsed=collapsed, autoZIndex=autoZIndex, **kwargs
)
self.draggable = draggable
self.base_layers: OrderedDict[str, str] = OrderedDict()
self.overlays: OrderedDict[str, str] = OrderedDict()
self.base_layers: OrderedDict[str, JsCode] = OrderedDict()
self.overlays: OrderedDict[str, JsCode] = OrderedDict()

def reset(self) -> None:
self.base_layers = OrderedDict()
Expand All @@ -202,9 +191,9 @@ def render(self, **kwargs) -> None:
continue
key = item.layer_name
if not item.overlay:
self.base_layers[key] = item.get_name()
self.base_layers[key] = JsCode(item.get_name())
else:
self.overlays[key] = item.get_name()
self.overlays[key] = JsCode(item.get_name())
super().render()


Expand Down
26 changes: 11 additions & 15 deletions folium/plugins/mouse_position.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from branca.element import MacroElement
from jinja2 import Template

from folium.elements import JSCSSMixin
from folium.utilities import parse_options
from folium.template import Template
from folium.utilities import JsCode, TypeJsFunctionArg


class MousePosition(JSCSSMixin, MacroElement):
Expand All @@ -27,14 +27,14 @@ class MousePosition(JSCSSMixin, MacroElement):
longitude and latitude decimal degree values.
prefix : str, default ''
A string to be prepended to the coordinates.
lat_formatter : str, default None
lat_formatter : str or JsCode, optional
Custom Javascript function to format the latitude value.
lng_formatter : str, default None
lng_formatter : str or JsCode, optional
Custom Javascript function to format the longitude value.
Examples
--------
>>> fmtr = "function(num) {return L.Util.formatNum(num, 3) + ' º ';};"
>>> fmtr = "function(num) {return L.Util.formatNum(num, 3) + ' º '}"
>>> MousePosition(
... position="topright",
... separator=" | ",
Expand All @@ -49,12 +49,8 @@ class MousePosition(JSCSSMixin, MacroElement):
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = new L.Control.MousePosition(
{{ this.options|tojson }}
{{ this.options|tojavascript }}
);
{{ this.get_name() }}.options["latFormatter"] =
{{ this.lat_formatter }};
{{ this.get_name() }}.options["lngFormatter"] =
{{ this.lng_formatter }};
{{ this._parent.get_name() }}.addControl({{ this.get_name() }});
{% endmacro %}
"""
Expand All @@ -81,21 +77,21 @@ def __init__(
lng_first=False,
num_digits=5,
prefix="",
lat_formatter=None,
lng_formatter=None,
lat_formatter: TypeJsFunctionArg = None,
lng_formatter: TypeJsFunctionArg = None,
**kwargs
):
super().__init__()
self._name = "MousePosition"

self.options = parse_options(
self.options = dict(
position=position,
separator=separator,
empty_string=empty_string,
lng_first=lng_first,
num_digits=num_digits,
prefix=prefix,
lat_formatter=JsCode.optional_create(lat_formatter),
lng_formatter=JsCode.optional_create(lng_formatter),
**kwargs
)
self.lat_formatter = lat_formatter or "undefined"
self.lng_formatter = lng_formatter or "undefined"
57 changes: 20 additions & 37 deletions folium/plugins/realtime.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Optional, Union
from typing import Union

from branca.element import MacroElement
from jinja2 import Template

from folium.elements import JSCSSMixin
from folium.utilities import JsCode, camelize, parse_options
from folium.template import Template
from folium.utilities import JsCode, TypeJsFunctionArg


class Realtime(JSCSSMixin, MacroElement):
Expand All @@ -27,11 +27,11 @@ class Realtime(JSCSSMixin, MacroElement):
on the map and stopped when layer is removed from the map
interval: int, default 60000
Automatic update interval, in milliseconds
get_feature_id: JsCode, optional
get_feature_id: str or JsCode, optional
A JS function with a geojson `feature` as parameter
default returns `feature.properties.id`
Function to get an identifier to uniquely identify a feature over time
update_feature: JsCode, optional
update_feature: str or JsCode, optional
A JS function with a geojson `feature` as parameter
Used to update an existing feature's layer;
by default, points (markers) are updated, other layers are discarded
Expand All @@ -44,15 +44,16 @@ class Realtime(JSCSSMixin, MacroElement):
Other keyword arguments are passed to the GeoJson layer, so you can pass
`style`, `point_to_layer` and/or `on_each_feature`.
`style`, `point_to_layer` and/or `on_each_feature`. Make sure to wrap
Javascript functions in the JsCode class.
Examples
--------
>>> from folium import JsCode
>>> m = folium.Map(location=[40.73, -73.94], zoom_start=12)
>>> rt = Realtime(
... "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson",
... get_feature_id=JsCode("(f) => { return f.properties.objectid; }"),
... get_feature_id="(f) => { return f.properties.objectid; }",
... point_to_layer=JsCode(
... "(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}"
... ),
Expand All @@ -64,18 +65,9 @@ class Realtime(JSCSSMixin, MacroElement):
_template = Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }}_options = {{ this.options|tojson }};
{% for key, value in this.functions.items() %}
{{ this.get_name() }}_options["{{key}}"] = {{ value }};
{% endfor %}
var {{ this.get_name() }} = new L.realtime(
{% if this.src is string or this.src is mapping -%}
{{ this.src|tojson }},
{% else -%}
{{ this.src.js_code }},
{% endif -%}
{{ this.get_name() }}_options
{{ this.src|tojavascript }},
{{ this.options|tojavascript }}
);
{{ this._parent.get_name() }}.addLayer(
{{ this.get_name() }}._container);
Expand All @@ -95,28 +87,19 @@ def __init__(
source: Union[str, dict, JsCode],
start: bool = True,
interval: int = 60000,
get_feature_id: Optional[JsCode] = None,
update_feature: Optional[JsCode] = None,
get_feature_id: TypeJsFunctionArg = None,
update_feature: TypeJsFunctionArg = None,
remove_missing: bool = False,
**kwargs
):
super().__init__()
self._name = "Realtime"
self.src = source

kwargs["start"] = start
kwargs["interval"] = interval
if get_feature_id is not None:
kwargs["get_feature_id"] = get_feature_id
if update_feature is not None:
kwargs["update_feature"] = update_feature
kwargs["remove_missing"] = remove_missing

# extract JsCode objects
self.functions = {}
for key, value in list(kwargs.items()):
if isinstance(value, JsCode):
self.functions[camelize(key)] = value.js_code
kwargs.pop(key)

self.options = parse_options(**kwargs)
self.options = dict(
start=start,
interval=interval,
get_feature_id=JsCode.optional_create(get_feature_id),
update_feature=JsCode.optional_create(update_feature),
remove_missing=remove_missing,
**kwargs
)
46 changes: 46 additions & 0 deletions folium/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json
from typing import Union

import jinja2

from folium.utilities import JsCode, TypeJsonValueNoNone, camelize


def tojavascript(obj: Union[str, JsCode, dict]) -> str:
if isinstance(obj, (str, JsCode)):
return obj
elif isinstance(obj, dict):
out = ["{\n"]
for key, value in obj.items():
if value is None:
continue
out.append(f' "{camelize(key)}": ')
if isinstance(value, JsCode):
out.append(value)
else:
out.append(_to_escaped_json(value))
out.append(",\n")
out.append("}")
return "".join(out)
else:
raise TypeError(f"Unsupported type: {type(obj)}")


def _to_escaped_json(obj: TypeJsonValueNoNone) -> 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
17 changes: 14 additions & 3 deletions folium/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,19 @@ def get_and_assert_figure_root(obj: Element) -> Figure:
return figure


class JsCode:
class JsCode(str):
"""Wrapper around Javascript code."""

def __init__(self, js_code: str):
self.js_code = js_code
@staticmethod
def optional_create(value: "TypeJsFunctionArg") -> Optional["JsCode"]:
"""Return a JsCode object if value is not None."""
if value is None:
return None
elif value is JsCode:
return value
else:
assert isinstance(value, str)
return JsCode(value)


TypeJsFunctionArg = Union[None, str, JsCode]

0 comments on commit 2725ffe

Please sign in to comment.