Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
python-visualization
GitHub Repository: python-visualization/folium
Path: blob/main/tests/test_features.py
1593 views
1
""" "
2
Folium Features Tests
3
---------------------
4
5
"""
6
7
import json
8
import os
9
import warnings
10
11
import pytest
12
from branca.element import Element
13
14
import folium
15
from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup
16
from folium.elements import EventHandler
17
from folium.utilities import JsCode
18
19
20
@pytest.fixture
21
def tmpl():
22
yield (
23
"""
24
<!DOCTYPE html>
25
<html>
26
<head>
27
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
28
</head>
29
<body>
30
</body>
31
<script>
32
</script>
33
</html>
34
"""
35
) # noqa
36
37
38
# Root path variable
39
rootpath = os.path.abspath(os.path.dirname(__file__))
40
41
42
# Figure
43
def test_figure_creation():
44
f = folium.Figure()
45
assert isinstance(f, Element)
46
47
bounds = f.get_bounds()
48
assert bounds == [[None, None], [None, None]], bounds
49
50
51
def test_figure_rendering():
52
f = folium.Figure()
53
out = f.render()
54
assert type(out) is str
55
56
bounds = f.get_bounds()
57
assert bounds == [[None, None], [None, None]], bounds
58
59
60
def test_figure_html(tmpl):
61
f = folium.Figure()
62
out = f.render()
63
out = os.linesep.join([s.strip() for s in out.splitlines() if s.strip()])
64
tmpl = os.linesep.join([s.strip() for s in tmpl.splitlines() if s.strip()])
65
assert out == tmpl, "\n" + out + "\n" + "-" * 80 + "\n" + tmpl
66
67
bounds = f.get_bounds()
68
assert bounds == [[None, None], [None, None]], bounds
69
70
71
def test_figure_double_rendering():
72
f = folium.Figure()
73
out = f.render()
74
out2 = f.render()
75
assert out == out2
76
77
bounds = f.get_bounds()
78
assert bounds == [[None, None], [None, None]], bounds
79
80
81
def test_marker_popups():
82
m = Map()
83
folium.Marker([45, -180], popup="-180").add_to(m)
84
folium.Marker([45, -120], popup=Popup("-120")).add_to(m)
85
folium.RegularPolygonMarker([45, -60], popup="-60").add_to(m)
86
folium.RegularPolygonMarker([45, 0], popup=Popup("0")).add_to(m)
87
folium.CircleMarker([45, 60], popup="60").add_to(m)
88
folium.CircleMarker([45, 120], popup=Popup("120")).add_to(m)
89
folium.CircleMarker([45, 90], popup=Popup("90"), weight=0).add_to(m)
90
m._repr_html_()
91
92
bounds = m.get_bounds()
93
assert bounds == [[45, -180], [45, 120]], bounds
94
95
96
# DivIcon.
97
def test_divicon():
98
html = """<svg height="100" width="100">
99
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
100
</svg>""" # noqa
101
div = folium.DivIcon(html=html)
102
assert isinstance(div, Element)
103
assert div.options["class_name"] == "empty"
104
assert div.options["html"] == html
105
106
107
# ColorLine.
108
def test_color_line():
109
m = Map([22.5, 22.5], zoom_start=3)
110
color_line = folium.ColorLine(
111
[[0, 0], [0, 45], [45, 45], [45, 0], [0, 0]],
112
[0, 1, 2, 3],
113
colormap=["b", "g", "y", "r"],
114
nb_steps=4,
115
weight=10,
116
opacity=1,
117
)
118
m.add_child(color_line)
119
m._repr_html_()
120
121
122
@pytest.fixture
123
def vegalite_spec(version):
124
file_version = "v1" if version == 1 else "vlater"
125
file = os.path.join(rootpath, "vegalite_data", f"vegalite_{file_version}.json")
126
127
if not os.path.exists(file):
128
raise FileNotFoundError(f"The vegalite data {file} does not exist.")
129
130
with open(file) as f:
131
spec = json.load(f)
132
133
if version is None or "$schema" in spec:
134
return spec
135
136
# Sample versions that might show up
137
schema_version = {2: "v2.6.0", 3: "v3.6.0", 4: "v4.6.0", 5: "v5.1.0"}[version]
138
spec["$schema"] = f"https://vega.github.io/schema/vega-lite/{schema_version}.json"
139
140
return spec
141
142
143
@pytest.mark.parametrize("version", [1, 2, 3, 4, 5, None])
144
def test_vegalite_major_version(vegalite_spec, version):
145
vegalite = folium.features.VegaLite(vegalite_spec)
146
147
if version is None:
148
assert vegalite.vegalite_major_version is None
149
else:
150
assert vegalite.vegalite_major_version == version
151
152
153
# GeoJsonTooltip GeometryCollection
154
def test_geojson_tooltip():
155
m = folium.Map([30.5, -97.5], zoom_start=10)
156
folium.GeoJson(
157
os.path.join(rootpath, "kuntarajat.geojson"),
158
tooltip=folium.GeoJsonTooltip(fields=["code", "name"]),
159
).add_to(m)
160
with warnings.catch_warnings(record=True) as w:
161
warnings.simplefilter("always")
162
m._repr_html_()
163
assert issubclass(
164
w[-1].category, UserWarning
165
), "GeoJsonTooltip GeometryCollection test failed."
166
167
168
# GeoJsonMarker type validation.
169
def test_geojson_marker():
170
m = folium.Map([30.4, -97.5], zoom_start=10)
171
with pytest.raises(TypeError):
172
folium.GeoJson(
173
os.path.join(rootpath, "subwaystations.geojson"), marker=ClickForMarker()
174
).add_to(m)
175
176
177
def test_geojson_find_identifier():
178
def _create(*properties):
179
return {
180
"type": "FeatureCollection",
181
"features": [
182
{"type": "Feature", "properties": item} for item in properties
183
],
184
}
185
186
def _assert_id_got_added(data):
187
_geojson = GeoJson(data)
188
assert _geojson.find_identifier() == "feature.id"
189
assert _geojson.data["features"][0]["id"] == "0"
190
191
data_with_id = _create(None, None)
192
data_with_id["features"][0]["id"] = "this-is-an-id"
193
data_with_id["features"][1]["id"] = "this-is-another-id"
194
geojson = GeoJson(data_with_id)
195
assert geojson.find_identifier() == "feature.id"
196
assert geojson.data["features"][0]["id"] == "this-is-an-id"
197
198
data_with_unique_properties = _create(
199
{"property-key": "some-value"},
200
{"property-key": "another-value"},
201
)
202
geojson = GeoJson(data_with_unique_properties)
203
assert geojson.find_identifier() == "feature.properties.property-key"
204
205
data_with_unique_properties = _create(
206
{"property-key": 42},
207
{"property-key": 43},
208
{"property-key": "or a string"},
209
)
210
geojson = GeoJson(data_with_unique_properties)
211
assert geojson.find_identifier() == "feature.properties.property-key"
212
213
# The test cases below have no id field or unique property,
214
# so an id will be added to the data.
215
216
data_with_identical_ids = _create(None, None)
217
data_with_identical_ids["features"][0]["id"] = "identical-ids"
218
data_with_identical_ids["features"][1]["id"] = "identical-ids"
219
_assert_id_got_added(data_with_identical_ids)
220
221
data_with_some_missing_ids = _create(None, None)
222
data_with_some_missing_ids["features"][0]["id"] = "this-is-an-id"
223
# the second feature doesn't have an id
224
_assert_id_got_added(data_with_some_missing_ids)
225
226
data_with_identical_properties = _create(
227
{"property-key": "identical-value"},
228
{"property-key": "identical-value"},
229
)
230
_assert_id_got_added(data_with_identical_properties)
231
232
data_bare = _create(None)
233
_assert_id_got_added(data_bare)
234
235
data_empty_dict = _create({})
236
_assert_id_got_added(data_empty_dict)
237
238
data_without_properties = _create(None)
239
del data_without_properties["features"][0]["properties"]
240
_assert_id_got_added(data_without_properties)
241
242
data_some_without_properties = _create({"key": "value"}, "will be deleted")
243
# the first feature has properties, but the second doesn't
244
del data_some_without_properties["features"][1]["properties"]
245
_assert_id_got_added(data_some_without_properties)
246
247
data_with_nested_properties = _create(
248
{
249
"summary": {"distance": 343.2},
250
"way_points": [3, 5],
251
}
252
)
253
_assert_id_got_added(data_with_nested_properties)
254
255
data_with_incompatible_properties = _create(
256
{
257
"summary": {"distances": [0, 6], "durations": None},
258
"way_points": [3, 5],
259
}
260
)
261
_assert_id_got_added(data_with_incompatible_properties)
262
263
data_loose_geometry = {
264
"type": "LineString",
265
"coordinates": [
266
[3.961389, 43.583333],
267
[3.968056, 43.580833],
268
[3.974722, 43.578333],
269
[3.986389, 43.575278],
270
[3.998333, 43.5725],
271
[4.163333, 43.530556],
272
],
273
}
274
geojson = GeoJson(data_loose_geometry)
275
geojson.convert_to_feature_collection()
276
assert geojson.find_identifier() == "feature.id"
277
assert geojson.data["features"][0]["id"] == "0"
278
279
280
def test_geojson_empty_features_with_styling():
281
# test we don't fail style function validation when there are no features
282
m = Map()
283
data = {"type": "FeatureCollection", "features": []}
284
GeoJson(data, style_function=lambda x: {}).add_to(m)
285
m.get_root().render()
286
287
288
def test_geojson_event_handler():
289
"""Test that event handlers are properly generated"""
290
m = Map()
291
data = {"type": "FeatureCollection", "features": []}
292
geojson = GeoJson(data, style_function=lambda x: {}).add_to(m)
293
fn = JsCode(
294
"""
295
function f(e) {
296
console.log("only for testing")
297
}
298
"""
299
)
300
geojson.add_child(EventHandler("mouseover", fn))
301
rendered = m.get_root().render()
302
assert fn.js_code in rendered
303
304
305
def test_geometry_collection_get_bounds():
306
"""Assert #1599 is fixed"""
307
geojson_data = {
308
"geometries": [
309
{
310
"coordinates": [
311
[
312
[-1, 1],
313
[0, 2],
314
[-3, 4],
315
[2, 0],
316
]
317
],
318
"type": "Polygon",
319
},
320
],
321
"type": "GeometryCollection",
322
}
323
assert folium.GeoJson(geojson_data).get_bounds() == [[0, -3], [4, 2]]
324
325
326
def test_choropleth_get_by_key():
327
geojson_data = {
328
"id": "0",
329
"type": "Feature",
330
"properties": {"idx": 0, "value": 78.0},
331
"geometry": {
332
"type": "Polygon",
333
"coordinates": [
334
[
335
[1, 2],
336
[3, 4],
337
]
338
],
339
},
340
}
341
342
# Test with string path in key_on
343
assert Choropleth._get_by_key(geojson_data, "properties.idx") == 0
344
assert Choropleth._get_by_key(geojson_data, "properties.value") == 78.0
345
346
# Test with combined string path and numerical index in key_on
347
assert Choropleth._get_by_key(geojson_data, "geometry.coordinates.0.0") == [1, 2]
348
349