Skip to content

Commit 05ca372

Browse files
committed
Explicit coordinate validation
Rewrite the validation functions in utilities that check single and multiple coordinate pairs. Make them more strict and raise meaningful exceptions. Apply the validation functions throughout the code base wherever possible. For the Marker class and vector classes a new BaseMarker class is added to make the distinction between single and multiple locations explicit. For the HeatMap and FastMarkerCluster classes I made custom validation in the classes themselves. Their use cases are different, so they cannot use the standard validation functions.
1 parent 36e4118 commit 05ca372

16 files changed

+331
-157
lines changed

folium/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import (absolute_import, division, print_function)
44

55
import sys
6-
import warnings
76

87
import branca
98
from branca.colormap import (ColorMap, LinearColormap, StepColormap)

folium/features.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from folium.folium import Map
2020
from folium.map import (FeatureGroup, Icon, Layer, Marker, Tooltip)
2121
from folium.utilities import (
22-
_iter_tolist,
22+
validate_locations,
2323
_parse_size,
2424
get_bounds,
2525
image_to_url,
@@ -45,7 +45,7 @@ class RegularPolygonMarker(Marker):
4545
4646
Parameters
4747
----------
48-
location: tuple or list, default None
48+
location: tuple or list
4949
Latitude and Longitude of Marker (Northing, Easting)
5050
number_of_sides: int, default 4
5151
Number of polygon sides
@@ -75,7 +75,7 @@ class RegularPolygonMarker(Marker):
7575
def __init__(self, location, number_of_sides=4, rotation=0, radius=15,
7676
popup=None, tooltip=None, **kwargs):
7777
super(RegularPolygonMarker, self).__init__(
78-
_iter_tolist(location),
78+
location,
7979
popup=popup, tooltip=tooltip
8080
)
8181
self._name = 'RegularPolygonMarker'
@@ -1357,6 +1357,7 @@ def __init__(self, positions, colors, colormap=None, nb_steps=12,
13571357
weight=None, opacity=None, **kwargs):
13581358
super(ColorLine, self).__init__(**kwargs)
13591359
self._name = 'ColorLine'
1360+
positions = validate_locations(positions)
13601361

13611362
if colormap is None:
13621363
cm = LinearColormap(['green', 'yellow', 'red'],

folium/folium.py

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

1515
from folium.map import FitBounds
1616
from folium.raster_layers import TileLayer
17-
from folium.utilities import _parse_size, _tmp_html, _validate_location
17+
from folium.utilities import _parse_size, _tmp_html, validate_location
1818

1919
from jinja2 import Environment, PackageLoader, Template
2020

@@ -243,7 +243,7 @@ def __init__(self, location=None, width='100%', height='100%',
243243
self.location = [0, 0]
244244
self.zoom_start = 1
245245
else:
246-
self.location = _validate_location(location)
246+
self.location = validate_location(location)
247247
self.zoom_start = zoom_start
248248

249249
Figure().add_child(self)

folium/map.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,9 @@
1111

1212
import warnings
1313

14-
from branca.element import CssLink, Element, Figure, Html, JavascriptLink, MacroElement # noqa
14+
from branca.element import Element, Figure, Html, MacroElement
1515

16-
from folium.utilities import (
17-
_validate_coordinates,
18-
camelize,
19-
get_bounds,
20-
parse_options,
21-
)
16+
from folium.utilities import validate_location, camelize, parse_options
2217

2318
from jinja2 import Template
2419

@@ -241,7 +236,7 @@ class Marker(MacroElement):
241236
242237
Parameters
243238
----------
244-
location: tuple or list, default None
239+
location: tuple or list
245240
Latitude and Longitude of Marker (Northing, Easting)
246241
popup: string or folium.Popup, default None
247242
Label for the Marker; either an escaped HTML string to initialize
@@ -278,30 +273,27 @@ def __init__(self, location, popup=None, tooltip=None, icon=None,
278273
draggable=False, **kwargs):
279274
super(Marker, self).__init__()
280275
self._name = 'Marker'
281-
self.location = _validate_coordinates(location)
276+
self.location = validate_location(location)
282277
self.options = parse_options(
283278
draggable=draggable or None,
284279
autoPan=draggable or None,
285280
**kwargs
286281
)
287282
if icon is not None:
288283
self.add_child(icon)
289-
if isinstance(popup, text_type) or isinstance(popup, binary_type):
290-
self.add_child(Popup(popup))
291-
elif popup is not None:
292-
self.add_child(popup)
293-
if isinstance(tooltip, Tooltip):
294-
self.add_child(tooltip)
295-
elif tooltip is not None:
296-
self.add_child(Tooltip(tooltip.__str__()))
284+
if popup is not None:
285+
self.add_child(popup if isinstance(popup, Popup)
286+
else Popup(str(popup)))
287+
if tooltip is not None:
288+
self.add_child(tooltip if isinstance(tooltip, Tooltip)
289+
else Tooltip(str(tooltip)))
297290

298291
def _get_self_bounds(self):
299-
"""
300-
Computes the bounds of the object itself (not including it's children)
301-
in the form [[lat_min, lon_min], [lat_max, lon_max]].
292+
"""Computes the bounds of the object itself.
302293
294+
Because a marker has only single coordinates, we repeat them.
303295
"""
304-
return get_bounds(self.location)
296+
return [self.location, self.location]
305297

306298

307299
class Popup(Element):

folium/plugins/antpath.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44

55
from branca.element import Figure, JavascriptLink
66

7-
from folium import Marker
8-
from folium.vector_layers import path_options
7+
from folium.vector_layers import path_options, BaseMultiLocation
98

109
from jinja2 import Template
1110

1211

13-
class AntPath(Marker):
12+
class AntPath(BaseMultiLocation):
1413
"""
1514
Class for drawing AntPath polyline overlays on a map.
1615
@@ -34,15 +33,15 @@ class AntPath(Marker):
3433
_template = Template(u"""
3534
{% macro script(this, kwargs) %}
3635
{{ this.get_name() }} = L.polyline.antPath(
37-
{{ this.location|tojson }},
36+
{{ this.locations|tojson }},
3837
{{ this.options|tojson }}
3938
).addTo({{this._parent.get_name()}});
4039
{% endmacro %}
4140
""")
4241

4342
def __init__(self, locations, popup=None, tooltip=None, **kwargs):
4443
super(AntPath, self).__init__(
45-
location=locations,
44+
locations,
4645
popup=popup,
4746
tooltip=tooltip,
4847
)

folium/plugins/boat_marker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from branca.element import Figure, JavascriptLink
66

77
from folium.map import Marker
8-
from folium.utilities import _validate_location, parse_options
8+
from folium.utilities import parse_options
99

1010
from jinja2 import Template
1111

@@ -50,7 +50,7 @@ class BoatMarker(Marker):
5050
def __init__(self, location, popup=None, icon=None,
5151
heading=0, wind_heading=None, wind_speed=0, **kwargs):
5252
super(BoatMarker, self).__init__(
53-
_validate_location(location),
53+
location,
5454
popup=popup,
5555
icon=icon
5656
)

folium/plugins/fast_marker_cluster.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import (absolute_import, division, print_function)
44

55
from folium.plugins.marker_cluster import MarkerCluster
6-
from folium.utilities import _validate_coordinates
6+
from folium.utilities import validate_location, if_pandas_df_convert_to_numpy
77

88
from jinja2 import Template
99

@@ -22,14 +22,15 @@ class FastMarkerCluster(MarkerCluster):
2222
2323
Parameters
2424
----------
25-
data: list
26-
List of list of shape [[], []]. Data points should be of
27-
the form [[lat, lng]].
28-
callback: string, default None
25+
data: list of list with values
26+
List of list of shape [[lat, lon], [lat, lon], etc.]
27+
When you use a custom callback you could add more values after the
28+
lat and lon. E.g. [[lat, lon, 'red'], [lat, lon, 'blue']]
29+
callback: string, optional
2930
A string representation of a valid Javascript function
30-
that will be passed a lat, lon coordinate pair. See the
31+
that will be passed each row in data. See the
3132
FasterMarkerCluster for an example of a custom callback.
32-
name : string, default None
33+
name : string, optional
3334
The name of the Layer, as it will appear in LayerControls.
3435
overlay : bool, default True
3536
Adds the layer as an optional overlay (True) or the base layer (False).
@@ -38,16 +39,16 @@ class FastMarkerCluster(MarkerCluster):
3839
show: bool, default True
3940
Whether the layer will be shown on opening (only for overlays).
4041
**kwargs
41-
Additional arguments are passed to Leaflet.markercluster. See
42-
https://github.com/Leaflet/Leaflet.markercluster for options.
42+
Additional arguments are passed to Leaflet.markercluster options. See
43+
https://github.com/Leaflet/Leaflet.markercluster
4344
4445
"""
4546
_template = Template(u"""
4647
{% macro script(this, kwargs) %}
4748
var {{ this.get_name() }} = (function(){
48-
{{ this._callback }}
49+
{{ this.callback }}
4950
50-
var data = {{ this._data|tojson }};
51+
var data = {{ this.data|tojson }};
5152
var cluster = L.markerClusterGroup({{ this.options|tojson }});
5253
5354
for (var i = 0; i < data.length; i++) {
@@ -69,15 +70,17 @@ def __init__(self, data, callback=None, options=None,
6970
control=control, show=show,
7071
**kwargs)
7172
self._name = 'FastMarkerCluster'
72-
self._data = _validate_coordinates(data)
73+
data = if_pandas_df_convert_to_numpy(data)
74+
self.data = [[*validate_location(row[:2]), *row[2:]] # noqa: E999
75+
for row in data]
7376

7477
if callback is None:
75-
self._callback = """
78+
self.callback = """
7679
var callback = function (row) {
7780
var icon = L.AwesomeMarkers.icon();
7881
var marker = L.marker(new L.LatLng(row[0], row[1]));
7982
marker.setIcon(icon);
8083
return marker;
8184
};"""
8285
else:
83-
self._callback = 'var callback = {};'.format(callback)
86+
self.callback = 'var callback = {};'.format(callback)

folium/plugins/heat_map.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66

77
from folium.map import Layer
88
from folium.utilities import (
9-
_isnan,
10-
_iter_tolist,
119
none_max,
1210
none_min,
1311
parse_options,
12+
if_pandas_df_convert_to_numpy,
13+
validate_location,
1414
)
1515

1616
from jinja2 import Template
1717

18+
import numpy as np
19+
1820

1921
class HeatMap(Layer):
2022
"""
@@ -61,12 +63,12 @@ def __init__(self, data, name=None, min_opacity=0.5, max_zoom=18,
6163
overlay=True, control=True, show=True, **kwargs):
6264
super(HeatMap, self).__init__(name=name, overlay=overlay,
6365
control=control, show=show)
64-
data = _iter_tolist(data)
65-
if _isnan(data):
66-
raise ValueError('data cannot contain NaNs, '
67-
'got:\n{!r}'.format(data))
6866
self._name = 'HeatMap'
69-
self.data = [[x for x in line] for line in data]
67+
data = if_pandas_df_convert_to_numpy(data)
68+
self.data = [[*validate_location(line[:2]), *line[2:]] # noqa: E999
69+
for line in data]
70+
if np.any(np.isnan(self.data)):
71+
raise ValueError('data may not contain NaNs.')
7072
self.options = parse_options(
7173
min_opacity=min_opacity,
7274
max_zoom=max_zoom,

folium/plugins/marker_cluster.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from branca.element import CssLink, Figure, JavascriptLink
66

7-
from folium.map import Icon, Layer, Marker, Popup
8-
from folium.utilities import parse_options
7+
from folium.map import Layer, Marker
8+
from folium.utilities import validate_locations, parse_options
99

1010
from jinja2 import Template
1111

@@ -71,25 +71,17 @@ def __init__(self, locations=None, popups=None, icons=None, name=None,
7171
self._name = 'MarkerCluster'
7272

7373
if locations is not None:
74-
if popups is None:
75-
popups = [None] * len(locations)
76-
if icons is None:
77-
icons = [None] * len(locations)
78-
for location, popup, icon in zip(locations, popups, icons):
79-
p = popup if self._validate(popup, Popup) else Popup(popup)
80-
i = icon if self._validate(icon, Icon) else Icon(icon)
81-
self.add_child(Marker(location, popup=p, icon=i))
74+
locations = validate_locations(locations)
75+
for i, location in enumerate(locations):
76+
self.add_child(Marker(location,
77+
popup=popups and popups[i],
78+
icon=icons and icons[i]))
8279

8380
self.options = parse_options(**kwargs)
8481
if icon_create_function is not None:
8582
assert isinstance(icon_create_function, str)
8683
self.icon_create_function = icon_create_function
8784

88-
@staticmethod
89-
def _validate(obj, cls):
90-
"""Check whether the given object is from the given class or is None."""
91-
return True if obj is None or isinstance(obj, cls) else False
92-
9385
def render(self, **kwargs):
9486
super(MarkerCluster, self).render(**kwargs)
9587

0 commit comments

Comments
 (0)