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