Skip to content

Commit 1739267

Browse files
committed
Use Jinja2 tojson filter
Don't `json.dumps` options and data, but use Jinja2's `tojson` filter to convert Python variables to JS compatible formats. This works for strings, numbers, bools, lists and dictionaries. I added a test file to verify this behavior. Normalize the behavior of options. In `__init__`, put options in a `self.options` dictionary using a new `parse_options()` function. Also allow `**kwargs` whenever it makes sense. I also did refactoring of templates whenever I touched them. Have consistent indentation and style. Fixed the tests for the changes. Consistently use the `normalize` function to normalize rendered output.
1 parent 8a7f4ad commit 1739267

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1222
-1260
lines changed

folium/features.py

Lines changed: 83 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
image_to_url,
2626
none_max,
2727
none_min,
28-
get_obj_in_upper_tree
28+
get_obj_in_upper_tree,
29+
parse_options,
2930
)
30-
from folium.vector_layers import PolyLine
31+
from folium.vector_layers import PolyLine, path_options
3132

3233
from jinja2 import Template
3334

@@ -46,65 +47,44 @@ class RegularPolygonMarker(Marker):
4647
----------
4748
location: tuple or list, default None
4849
Latitude and Longitude of Marker (Northing, Easting)
49-
color: string, default 'black'
50-
Marker line color
51-
opacity: float, default 1
52-
Line opacity, scale 0-1
53-
weight: int, default 2
54-
Stroke weight in pixels
55-
fill_color: string, default 'blue'
56-
Marker fill color
57-
fill_opacity: float, default 1
58-
Marker fill opacity
5950
number_of_sides: int, default 4
6051
Number of polygon sides
6152
rotation: int, default 0
6253
Rotation angle in degrees
6354
radius: int, default 15
6455
Marker radius, in pixels
65-
popup: string or folium.Popup, default None
56+
popup: string or Popup, optional
6657
Input text or visualization for object displayed when clicking.
67-
tooltip: str or folium.Tooltip, default None
58+
tooltip: str or folium.Tooltip, optional
6859
Display a text when hovering over the object.
60+
**kwargs:
61+
See vector layers path_options for additional arguments.
6962
7063
https://humangeo.github.io/leaflet-dvf/
7164
7265
"""
7366
_template = Template(u"""
74-
{% macro script(this, kwargs) %}
75-
var {{this.get_name()}} = new L.RegularPolygonMarker(
76-
new L.LatLng({{this.location[0]}},{{this.location[1]}}),
77-
{
78-
icon : new L.Icon.Default(),
79-
color: '{{this.color}}',
80-
opacity: {{this.opacity}},
81-
weight: {{this.weight}},
82-
fillColor: '{{this.fill_color}}',
83-
fillOpacity: {{this.fill_opacity}},
84-
numberOfSides: {{this.number_of_sides}},
85-
rotation: {{this.rotation}},
86-
radius: {{this.radius}}
87-
}
88-
).addTo({{this._parent.get_name()}});
89-
{% endmacro %}
90-
""")
67+
{% macro script(this, kwargs) %}
68+
var {{ this.get_name() }} = new L.RegularPolygonMarker(
69+
{{ this.location|tojson }},
70+
{{ this.options|tojson }}
71+
).addTo({{ this._parent.get_name() }});
72+
{% endmacro %}
73+
""")
9174

92-
def __init__(self, location, color='black', opacity=1, weight=2,
93-
fill_color='blue', fill_opacity=1, number_of_sides=4,
94-
rotation=0, radius=15, popup=None, tooltip=None):
75+
def __init__(self, location, number_of_sides=4, rotation=0, radius=15,
76+
popup=None, tooltip=None, **kwargs):
9577
super(RegularPolygonMarker, self).__init__(
9678
_iter_tolist(location),
9779
popup=popup, tooltip=tooltip
9880
)
9981
self._name = 'RegularPolygonMarker'
100-
self.color = color
101-
self.opacity = opacity
102-
self.weight = weight
103-
self.fill_color = fill_color
104-
self.fill_opacity = fill_opacity
105-
self.number_of_sides = number_of_sides
106-
self.rotation = rotation
107-
self.radius = radius
82+
self.options = path_options(**kwargs)
83+
self.options.update(parse_options(
84+
number_of_sides=number_of_sides,
85+
rotation=rotation,
86+
radius=radius,
87+
))
10888

10989
def render(self, **kwargs):
11090
"""Renders the HTML representation of the element."""
@@ -402,7 +382,7 @@ class GeoJson(Layer):
402382
function {{ this.get_name() }}_styler(feature) {
403383
switch({{ this.feature_identifier }}) {
404384
{%- for style, ids_list in this.style_map.items() if not style == 'default' %}
405-
{% for id_val in ids_list %}case "{{ id_val }}": {% endfor %}
385+
{% for id_val in ids_list %}case {{ id_val|tojson }}: {% endfor %}
406386
return {{ style }};
407387
{%- endfor %}
408388
default:
@@ -414,7 +394,7 @@ class GeoJson(Layer):
414394
function {{ this.get_name() }}_highlighter(feature) {
415395
switch({{ this.feature_identifier }}) {
416396
{%- for style, ids_list in this.highlight_map.items() if not style == 'default' %}
417-
{% for id_val in ids_list %}case "{{ id_val }}": {% endfor %}
397+
{% for id_val in ids_list %}case {{ id_val|tojson }}: {% endfor %}
418398
return {{ style }};
419399
{%- endfor %}
420400
default:
@@ -439,17 +419,17 @@ class GeoJson(Layer):
439419
};
440420
var {{ this.get_name() }} = L.geoJson(null, {
441421
{%- if this.smooth_factor is not none %}
442-
smoothFactor: {{ this.smooth_factor }},
422+
smoothFactor: {{ this.smooth_factor|tojson }},
443423
{%- endif %}
444424
onEachFeature: {{ this.get_name() }}_onEachFeature,
445425
{% if this.style %}
446426
style: {{ this.get_name() }}_styler,
447427
{%- endif %}
448428
}).addTo({{ this._parent.get_name() }});
449429
{%- if this.embed %}
450-
{{ this.get_name() }}.addData({{ this.json }});
430+
{{ this.get_name() }}.addData({{ this.data|tojson }});
451431
{%- else %}
452-
$.ajax({url: "{{ this.embed_link }}", dataType: 'json', async: true,
432+
$.ajax({url: {{ this.embed_link|tojson }}, dataType: 'json', async: true,
453433
success: function(data) {
454434
{{ this.get_name() }}.addData(data);
455435
}});
@@ -582,8 +562,6 @@ def render(self, **kwargs):
582562
if self.highlight:
583563
self.highlight_map = mapper.get_highlight_map(
584564
self.highlight_function)
585-
if self.embed:
586-
self.json = json.dumps(self.data, sort_keys=True)
587565
super(GeoJson, self).render()
588566

589567

@@ -699,15 +677,21 @@ class TopoJson(Layer):
699677
"""
700678
_template = Template(u"""
701679
{% macro script(this, kwargs) %}
702-
var {{this.get_name()}}_data = {{this.style_data()}};
703-
var {{this.get_name()}} = L.geoJson(topojson.feature(
704-
{{this.get_name()}}_data,
705-
{{this.get_name()}}_data.{{this.object_path}})
706-
{% if this.smooth_factor is not none %}
707-
, {smoothFactor: {{this.smooth_factor}}}
708-
{% endif %}
709-
).addTo({{this._parent.get_name()}});
710-
{{this.get_name()}}.setStyle(function(feature) {return feature.properties.style;});
680+
var {{ this.get_name() }}_data = {{ this.data|tojson }};
681+
var {{ this.get_name() }} = L.geoJson(
682+
topojson.feature(
683+
{{ this.get_name() }}_data,
684+
{{ this.get_name() }}_data.{{ this.object_path }}
685+
),
686+
{
687+
{%- if this.smooth_factor is not none %}
688+
smoothFactor: {{ this.smooth_factor|tojson }},
689+
{%- endif %}
690+
}
691+
).addTo({{ this._parent.get_name() }});
692+
{{ this.get_name() }}.setStyle(function(feature) {
693+
return feature.properties.style;
694+
});
711695
{% endmacro %}
712696
""") # noqa
713697

@@ -743,11 +727,7 @@ def style_function(x):
743727
self.add_child(Tooltip(tooltip))
744728

745729
def style_data(self):
746-
"""
747-
Applies self.style_function to each feature of self.data and returns
748-
a corresponding JSON output.
749-
750-
"""
730+
"""Applies self.style_function to each feature of self.data."""
751731

752732
def recursive_get(data, keys):
753733
if len(keys):
@@ -758,10 +738,10 @@ def recursive_get(data, keys):
758738
geometries = recursive_get(self.data, self.object_path.split('.'))['geometries'] # noqa
759739
for feature in geometries:
760740
feature.setdefault('properties', {}).setdefault('style', {}).update(self.style_function(feature)) # noqa
761-
return json.dumps(self.data, sort_keys=True)
762741

763742
def render(self, **kwargs):
764743
"""Renders the HTML representation of the element."""
744+
self.style_data()
765745
super(TopoJson, self).render(**kwargs)
766746

767747
figure = self.get_root()
@@ -822,7 +802,7 @@ class GeoJsonTooltip(Tooltip):
822802
This will use JavaScript's .toLocaleString() to format 'clean' values
823803
as strings for the user's location; i.e. 1,000,000.00 comma separators,
824804
float truncation, etc.
825-
\*Available for most of JavaScript's primitive types (any data you'll
805+
Available for most of JavaScript's primitive types (any data you'll
826806
serve into the template).
827807
style: str, default None.
828808
HTML inline style properties like font and colors. Will be applied to
@@ -852,11 +832,11 @@ class GeoJsonTooltip(Tooltip):
852832
function(layer){
853833
// Convert non-primitive to String.
854834
let handleObject = (feature)=>typeof(feature)=='object' ? JSON.stringify(feature) : feature;
855-
let fields = {{ this.fields }};
856-
{% if this.aliases %}
857-
let aliases = {{ this.aliases }};
858-
{% endif %}
859-
return '<table{% if this.style %} style="{{this.style}}"{% endif%}>' +
835+
let fields = {{ this.fields|tojson }};
836+
{%- if this.aliases %}
837+
let aliases = {{ this.aliases|tojson }};
838+
{%- endif %}
839+
return '<table{% if this.style %} style={{ this.style|tojson }}{% endif%}>' +
860840
String(
861841
fields.map(
862842
columnname=>
@@ -872,7 +852,7 @@ class GeoJsonTooltip(Tooltip):
872852
{% if this.localize %}.toLocaleString(){% endif %}}</td></tr>`
873853
).join(''))
874854
+'</table>'
875-
}, {{ this.options }});
855+
}, {{ this.options|tojson }});
876856
{% endmacro %}
877857
""")
878858

@@ -1207,28 +1187,23 @@ class DivIcon(MacroElement):
12071187
"""
12081188

12091189
_template = Template(u"""
1210-
{% macro script(this, kwargs) %}
1211-
1212-
var {{this.get_name()}} = L.divIcon({
1213-
{% if this.icon_size %}iconSize: [{{this.icon_size[0]}},{{this.icon_size[1]}}],{% endif %}
1214-
{% if this.icon_anchor %}iconAnchor: [{{this.icon_anchor[0]}},{{this.icon_anchor[1]}}],{% endif %}
1215-
{% if this.popup_anchor %}popupAnchor: [{{this.popup_anchor[0]}},{{this.popup_anchor[1]}}],{% endif %}
1216-
{% if this.className %}className: '{{this.className}}',{% endif %}
1217-
{% if this.html %}html: `{{this.html}}`,{% endif %}
1218-
});
1219-
{{this._parent.get_name()}}.setIcon({{this.get_name()}});
1220-
{% endmacro %}
1221-
""") # noqa
1190+
{% macro script(this, kwargs) %}
1191+
var {{ this.get_name() }} = L.divIcon({{ this.options|tojson }});
1192+
{{this._parent.get_name()}}.setIcon({{this.get_name()}});
1193+
{% endmacro %}
1194+
""") # noqa
12221195

12231196
def __init__(self, html=None, icon_size=None, icon_anchor=None,
12241197
popup_anchor=None, class_name='empty'):
12251198
super(DivIcon, self).__init__()
12261199
self._name = 'DivIcon'
1227-
self.icon_size = icon_size
1228-
self.icon_anchor = icon_anchor
1229-
self.popup_anchor = popup_anchor
1230-
self.html = html
1231-
self.className = class_name
1200+
self.options = parse_options(
1201+
html=html,
1202+
icon_size=icon_size,
1203+
icon_ancher=icon_anchor,
1204+
popup_anchor=popup_anchor,
1205+
class_name=class_name,
1206+
)
12321207

12331208

12341209
class LatLngPopup(MacroElement):
@@ -1305,58 +1280,47 @@ class CustomIcon(Icon):
13051280
output file.
13061281
* If array-like, it will be converted to PNG base64 string
13071282
and embedded in the output.
1308-
icon_size : tuple of 2 int
1283+
icon_size : tuple of 2 int, optional
13091284
Size of the icon image in pixels.
1310-
icon_anchor : tuple of 2 int
1285+
icon_anchor : tuple of 2 int, optional
13111286
The coordinates of the "tip" of the icon
13121287
(relative to its top left corner).
13131288
The icon will be aligned so that this point is at the
13141289
marker's geographical location.
1315-
shadow_image : string, file or array-like object
1290+
shadow_image : string, file or array-like object, optional
13161291
The data for the shadow image. If not specified,
13171292
no shadow image will be created.
1318-
shadow_size : tuple of 2 int
1293+
shadow_size : tuple of 2 int, optional
13191294
Size of the shadow image in pixels.
1320-
shadow_anchor : tuple of 2 int
1295+
shadow_anchor : tuple of 2 int, optional
13211296
The coordinates of the "tip" of the shadow relative to its
13221297
top left corner (the same as icon_anchor if not specified).
1323-
popup_anchor : tuple of 2 int
1298+
popup_anchor : tuple of 2 int, optional
13241299
The coordinates of the point from which popups will "open",
13251300
relative to the icon anchor.
13261301
13271302
"""
13281303
_template = Template(u"""
1329-
{% macro script(this, kwargs) %}
1330-
1331-
var {{this.get_name()}} = L.icon({
1332-
iconUrl: '{{this.icon_url}}',
1333-
{% if this.icon_size %}iconSize: [{{this.icon_size[0]}},{{this.icon_size[1]}}],{% endif %}
1334-
{% if this.icon_anchor %}iconAnchor: [{{this.icon_anchor[0]}},{{this.icon_anchor[1]}}],{% endif %}
1335-
1336-
{% if this.shadow_url %}shadowUrl: '{{this.shadow_url}}',{% endif %}
1337-
{% if this.shadow_size %}shadowSize: [{{this.shadow_size[0]}},{{this.shadow_size[1]}}],{% endif %}
1338-
{% if this.shadow_anchor %}shadowAnchor: [{{this.shadow_anchor[0]}},{{this.shadow_anchor[1]}}],{% endif %}
1339-
1340-
{% if this.popup_anchor %}popupAnchor: [{{this.popup_anchor[0]}},{{this.popup_anchor[1]}}],{% endif %}
1341-
});
1342-
{{this._parent.get_name()}}.setIcon({{this.get_name()}});
1343-
{% endmacro %}
1344-
""") # noqa
1304+
{% macro script(this, kwargs) %}
1305+
var {{ this.get_name() }} = L.icon({{ this.options|tojson }});
1306+
{{ this._parent.get_name() }}.setIcon({{ this.get_name() }});
1307+
{% endmacro %}
1308+
""") # noqa
13451309

13461310
def __init__(self, icon_image, icon_size=None, icon_anchor=None,
13471311
shadow_image=None, shadow_size=None, shadow_anchor=None,
13481312
popup_anchor=None):
13491313
super(Icon, self).__init__()
13501314
self._name = 'CustomIcon'
1351-
self.icon_url = image_to_url(icon_image)
1352-
self.icon_size = icon_size
1353-
self.icon_anchor = icon_anchor
1354-
1355-
self.shadow_url = (image_to_url(shadow_image)
1356-
if shadow_image is not None else None)
1357-
self.shadow_size = shadow_size
1358-
self.shadow_anchor = shadow_anchor
1359-
self.popup_anchor = popup_anchor
1315+
self.options = parse_options(
1316+
icon_url=image_to_url(icon_image),
1317+
icon_size=icon_size,
1318+
icon_anchor=icon_anchor,
1319+
shadow_url=shadow_image and image_to_url(shadow_image),
1320+
shadow_size=shadow_size,
1321+
shadow_anchor=shadow_anchor,
1322+
popup_anchor=popup_anchor,
1323+
)
13601324

13611325

13621326
class ColorLine(FeatureGroup):

0 commit comments

Comments
 (0)