Skip to content

Commit bcd5948

Browse files
committed
refactor(plugin): simplify marker and popup handling in OMS
1 parent 7ec3548 commit bcd5948

File tree

3 files changed

+121
-177
lines changed

3 files changed

+121
-177
lines changed
Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
# OverlappingMarkerSpiderfier
22

3+
The `OverlappingMarkerSpiderfier` is a plugin for Folium that helps manage overlapping markers by "spiderfying" them when clicked, making it easier to select individual markers.
4+
35
```{code-cell} ipython3
46
import folium
5-
from folium import plugins
7+
from folium.plugins import OverlappingMarkerSpiderfier
68
79
# Create a map
8-
m = folium.Map(location=[45.05, 3.05], zoom_start=14)
9-
10-
# Generate some markers
11-
markers = [folium.Marker(location=[45.05 + i * 0.0001, 3.05 + i * 0.0001], options={'desc': f'Marker {i}'}) for i in range(10)]
10+
m = folium.Map(location=[45.05, 3.05], zoom_start=13)
1211
1312
# Add markers to the map
14-
for marker in markers:
15-
marker.add_to(m)
13+
for i in range(20):
14+
folium.Marker(
15+
location=[45.05 + i * 0.0001, 3.05 + i * 0.0001],
16+
popup=f"Marker {i}"
17+
).add_to(m)
1618
17-
# Add OverlappingMarkerSpiderfier
18-
oms = plugins.OverlappingMarkerSpiderfier(
19-
markers=markers,
20-
options={'keepSpiderfied': True, 'nearbyDistance': 20}
21-
).add_to(m)
19+
# Add the OverlappingMarkerSpiderfier plugin
20+
oms = OverlappingMarkerSpiderfier(options={
21+
"keepSpiderfied": True, # Markers remain spiderfied after clicking
22+
"nearbyDistance": 20, # Distance for clustering markers in pixel
23+
"circleSpiralSwitchover": 10, # Threshold for switching between circle and spiral
24+
"legWeight": 2.0 # Line thickness for spider legs
25+
})
26+
oms.add_to(m)
2227
23-
# Display the map
2428
m
29+
```
30+
Lines changed: 39 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,67 @@
11
from jinja2 import Template
2-
3-
from folium.elements import JSCSSMixin
4-
from folium.map import Layer
2+
from folium.elements import JSCSSMixin, MacroElement
53
from folium.utilities import parse_options
64

7-
8-
class OverlappingMarkerSpiderfier(JSCSSMixin, Layer):
9-
"""A plugin that handles overlapping markers by spreading them into a spider-like pattern.
10-
11-
This plugin uses the OverlappingMarkerSpiderfier-Leaflet library to manage markers
12-
that are close to each other or overlap. When clicked, the overlapping markers
13-
spread out in a spiral pattern, making them easier to select individually.
14-
5+
class OverlappingMarkerSpiderfier(JSCSSMixin, MacroElement):
6+
"""
7+
A plugin that handles overlapping markers on a map by spreading them out in a spiral or circle pattern when clicked.
8+
9+
This plugin is useful when you have multiple markers in close proximity that would otherwise be difficult to interact with.
10+
When a user clicks on a cluster of overlapping markers, they spread out in a 'spider' pattern, making each marker
11+
individually accessible.
12+
13+
Markers must be added to the map **before** calling `oms.add_to(map)`.
14+
The plugin identifies and manages all markers already present on the map.
15+
1516
Parameters
1617
----------
17-
markers : list, optional
18-
List of markers to be managed by the spiderfier
19-
name : string, optional
20-
Name of the layer control
21-
overlay : bool, default True
22-
Whether the layer will be included in LayerControl
23-
control : bool, default True
24-
Whether the layer will be included in LayerControl
25-
show : bool, default True
26-
Whether the layer will be shown on opening
2718
options : dict, optional
28-
Additional options to be passed to the OverlappingMarkerSpiderfier instance
29-
See https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet for available options
30-
19+
The options to configure the spiderfier behavior:
20+
- keepSpiderfied : bool, default True
21+
If true, markers stay spiderfied after clicking
22+
- nearbyDistance : int, default 20
23+
Pixels away from a marker that is considered overlapping
24+
- legWeight : float, default 1.5
25+
Weight of the spider legs
26+
- circleSpiralSwitchover : int, optional
27+
Number of markers at which to switch from circle to spiral pattern
28+
3129
Example
3230
-------
33-
>>> markers = [marker1, marker2, marker3] # Create some markers
34-
>>> spiderfier = OverlappingMarkerSpiderfier(
35-
... markers=markers, keepSpiderfied=True, nearbyDistance=20
36-
... )
37-
>>> spiderfier.add_to(m) # Add to your map
31+
>>> oms = OverlappingMarkerSpiderfier(options={
32+
... "keepSpiderfied": True,
33+
... "nearbyDistance": 30,
34+
... "legWeight": 2.0
35+
... })
36+
>>> oms.add_to(map)
3837
"""
39-
4038
_template = Template(
4139
"""
4240
{% macro script(this, kwargs) %}
43-
var {{ this.get_name() }} = (function () {
44-
var layerGroup = L.layerGroup();
45-
41+
(function () {
4642
try {
4743
var oms = new OverlappingMarkerSpiderfier(
4844
{{ this._parent.get_name() }},
4945
{{ this.options|tojson }}
5046
);
5147
52-
var popup = L.popup({
53-
offset: L.point(0, -30)
48+
oms.addListener('spiderfy', function() {
49+
{{ this._parent.get_name() }}.closePopup();
5450
});
5551
56-
oms.addListener('click', function(marker) {
57-
var content;
58-
if (marker.options && marker.options.options && marker.options.options.desc) {
59-
content = marker.options.options.desc;
60-
} else if (marker._popup && marker._popup._content) {
61-
content = marker._popup._content;
62-
} else {
63-
content = "";
64-
}
65-
66-
if (content) {
67-
popup.setContent(content);
68-
popup.setLatLng(marker.getLatLng());
69-
{{ this._parent.get_name() }}.openPopup(popup);
52+
{{ this._parent.get_name() }}.eachLayer(function(layer) {
53+
if (
54+
layer instanceof L.Marker
55+
) {
56+
oms.addMarker(layer);
7057
}
7158
});
7259
73-
oms.addListener('spiderfy', function(markers) {
74-
{{ this._parent.get_name() }}.closePopup();
75-
});
76-
77-
{% for marker in this.markers %}
78-
var {{ marker.get_name() }} = L.marker(
79-
{{ marker.location|tojson }},
80-
{{ marker.options|tojson }}
81-
);
82-
83-
{% if marker.popup %}
84-
{{ marker.get_name() }}.bindPopup({{ marker.popup.get_content()|tojson }});
85-
{% endif %}
86-
87-
oms.addMarker({{ marker.get_name() }});
88-
layerGroup.addLayer({{ marker.get_name() }});
89-
{% endfor %}
9060
} catch (error) {
91-
console.error('Error in OverlappingMarkerSpiderfier initialization:', error);
61+
console.error('Error initializing OverlappingMarkerSpiderfier:', error);
9262
}
93-
94-
return layerGroup;
9563
})();
9664
{% endmacro %}
97-
9865
"""
9966
)
10067

@@ -105,27 +72,14 @@ class OverlappingMarkerSpiderfier(JSCSSMixin, Layer):
10572
)
10673
]
10774

108-
def __init__(
109-
self,
110-
markers=None,
111-
name=None,
112-
overlay=True,
113-
control=True,
114-
show=True,
115-
options=None,
116-
**kwargs,
117-
):
118-
super().__init__(name=name, overlay=overlay, control=control, show=show)
75+
def __init__(self, options=None, **kwargs):
76+
super().__init__()
11977
self._name = "OverlappingMarkerSpiderfier"
120-
121-
self.markers = markers or []
122-
12378
default_options = {
12479
"keepSpiderfied": True,
12580
"nearbyDistance": 20,
12681
"legWeight": 1.5,
12782
}
12883
if options:
12984
default_options.update(options)
130-
13185
self.options = parse_options(**default_options, **kwargs)

tests/plugins/test_overlapping_marker_spiderfier.py

Lines changed: 63 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,30 @@
44
"""
55

66
import numpy as np
7-
from jinja2 import Template
87

9-
import folium
10-
from folium import plugins
11-
from folium.utilities import normalize
8+
from folium.folium import Map
9+
from folium.map import Marker
10+
from folium.plugins.overlapping_marker_spiderfier import OverlappingMarkerSpiderfier
1211

1312

14-
def test_overlapping_marker_spiderfier():
13+
def test_oms_js_inclusion():
14+
"""
15+
Test that the OverlappingMarkerSpiderfier JavaScript library is included in the map.
16+
"""
17+
m = Map([45.05, 3.05], zoom_start=14)
18+
oms = OverlappingMarkerSpiderfier().add_to(m)
19+
20+
rendered_map = m._parent.render()
21+
assert (
22+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"></script>'
23+
in rendered_map
24+
), "OverlappingMarkerSpiderfier JS file is missing in the rendered output."
25+
26+
27+
def test_marker_addition():
28+
"""
29+
Test that markers are correctly added to the map.
30+
"""
1531
N = 10
1632
np.random.seed(seed=26082009)
1733
data = np.array(
@@ -21,92 +37,60 @@ def test_overlapping_marker_spiderfier():
2137
]
2238
).T
2339

24-
m = folium.Map([45.05, 3.05], zoom_start=14)
25-
markers = [
26-
folium.Marker(location=loc, popup=f"Marker {i}") for i, loc in enumerate(data)
27-
]
40+
m = Map([45.05, 3.05], zoom_start=14)
41+
markers = [Marker(location=loc, popup=f"Marker {
42+
i}") for i, loc in enumerate(data)]
2843

2944
for marker in markers:
3045
marker.add_to(m)
3146

32-
oms = plugins.OverlappingMarkerSpiderfier(
33-
markers=markers, options={"keepSpiderfied": True, "nearbyDistance": 20}
34-
).add_to(m)
35-
36-
tmpl_for_expected = Template(
37-
"""
38-
var {{this.get_name()}} = (function () {
39-
var layerGroup = L.layerGroup();
40-
try {
41-
var oms = new OverlappingMarkerSpiderfier(
42-
{{ this._parent.get_name() }},
43-
{{ this.options|tojson }}
44-
);
45-
46-
var popup = L.popup({
47-
offset: L.point(0, -30)
48-
});
49-
50-
oms.addListener('click', function(marker) {
51-
var content;
52-
if (marker.options && marker.options.options && marker.options.options.desc) {
53-
content = marker.options.options.desc;
54-
} else if (marker._popup && marker._popup._content) {
55-
content = marker._popup._content;
56-
} else {
57-
content = "";
58-
}
59-
60-
if (content) {
61-
popup.setContent(content);
62-
popup.setLatLng(marker.getLatLng());
63-
{{ this._parent.get_name() }}.openPopup(popup);
64-
}
65-
});
66-
67-
oms.addListener('spiderfy', function(markers) {
68-
{{ this._parent.get_name() }}.closePopup();
69-
});
70-
71-
{% for marker in this.markers %}
72-
var {{ marker.get_name() }} = L.marker(
73-
{{ marker.location|tojson }},
74-
{{ marker.options|tojson }}
75-
);
76-
77-
{% if marker.popup %}
78-
{{ marker.get_name() }}.bindPopup({{ marker.popup.get_content()|tojson }});
79-
{% endif %}
80-
81-
oms.addMarker({{ marker.get_name() }});
82-
layerGroup.addLayer({{ marker.get_name() }});
83-
{% endfor %}
84-
} catch (error) {
85-
console.error('Error in OverlappingMarkerSpiderfier initialization:', error);
86-
}
87-
88-
return layerGroup;
89-
})();
90-
"""
47+
assert len(m._children) == len(markers) + 1, (
48+
f"Expected {len(markers)} markers on the map, but found {
49+
len(m._children) - 1}."
9150
)
92-
expected = normalize(tmpl_for_expected.render(this=oms))
9351

94-
out = normalize(m._parent.render())
9552

96-
assert (
97-
'<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"></script>'
98-
in out
99-
)
53+
def test_map_bounds():
54+
"""
55+
Test that the map bounds correctly encompass all added markers.
56+
"""
57+
N = 10
58+
np.random.seed(seed=26082009)
59+
data = np.array(
60+
[
61+
np.random.uniform(low=45.0, high=45.1, size=N),
62+
np.random.uniform(low=3.0, high=3.1, size=N),
63+
]
64+
).T
10065

101-
assert expected in out
66+
m = Map([45.05, 3.05], zoom_start=14)
67+
markers = [Marker(location=loc, popup=f"Marker {
68+
i}") for i, loc in enumerate(data)]
69+
70+
for marker in markers:
71+
marker.add_to(m)
10272

10373
bounds = m.get_bounds()
10474
assert bounds is not None, "Map bounds should not be None"
10575

10676
min_lat, min_lon = data.min(axis=0)
10777
max_lat, max_lon = data.max(axis=0)
10878

109-
assert bounds[0][0] <= min_lat
110-
assert bounds[0][1] <= min_lon
111-
assert bounds[1][0] >= max_lat
112-
assert bounds[1][1] >= max_lon
79+
assert bounds[0][0] <= min_lat, "Map bounds do not correctly include the minimum latitude."
80+
assert bounds[0][1] <= min_lon, "Map bounds do not correctly include the minimum longitude."
81+
assert bounds[1][0] >= max_lat, "Map bounds do not correctly include the maximum latitude."
82+
assert bounds[1][1] >= max_lon, "Map bounds do not correctly include the maximum longitude."
83+
84+
85+
def test_overlapping_marker_spiderfier_integration():
86+
"""
87+
Test that OverlappingMarkerSpiderfier integrates correctly with the map.
88+
"""
89+
m = Map([45.05, 3.05], zoom_start=14)
90+
oms = OverlappingMarkerSpiderfier(
91+
options={"keepSpiderfied": True, "nearbyDistance": 20})
92+
oms.add_to(m)
93+
94+
assert oms.get_name() in m._children, (
95+
f"OverlappingMarkerSpiderfier is not correctly added to the map."
96+
)

0 commit comments

Comments
 (0)