Skip to content

Commit 588670c

Browse files
authored
Merge pull request #933 from Conengmo/sync
Dual map plugin
2 parents 39ca695 + acbff1b commit 588670c

File tree

7 files changed

+513
-116
lines changed

7 files changed

+513
-116
lines changed

examples/Plugins.ipynb

Lines changed: 129 additions & 115 deletions
Large diffs are not rendered by default.

examples/plugin-DualMap.ipynb

Lines changed: 193 additions & 0 deletions
Large diffs are not rendered by default.

folium/plugins/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from folium.plugins.beautify_icon import BeautifyIcon
1515
from folium.plugins.boat_marker import BoatMarker
1616
from folium.plugins.draw import Draw
17+
from folium.plugins.dual_map import DualMap
1718
from folium.plugins.fast_marker_cluster import FastMarkerCluster
1819
from folium.plugins.feature_group_sub_group import FeatureGroupSubGroup
1920
from folium.plugins.float_image import FloatImage
@@ -37,6 +38,7 @@
3738
'BeautifyIcon',
3839
'BoatMarker',
3940
'Draw',
41+
'DualMap',
4042
'FastMarkerCluster',
4143
'FeatureGroupSubGroup',
4244
'FloatImage',

folium/plugins/dual_map.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from jinja2 import Template
2+
3+
from branca.element import MacroElement, Figure, JavascriptLink
4+
5+
from folium.folium import Map
6+
from folium.utilities import deep_copy
7+
8+
9+
class DualMap(MacroElement):
10+
"""Create two maps in the same window.
11+
12+
Adding children to this objects adds them to both maps. You can access
13+
the individual maps with `DualMap.m1` and `DualMap.m2`.
14+
15+
Uses the Leaflet plugin Sync: https://github.com/jieter/Leaflet.Sync
16+
17+
Parameters
18+
----------
19+
layout : {'horizontal', 'vertical'}
20+
Select how the two maps should be positioned. Either horizontal (left
21+
and right) or vertical (top and bottom).
22+
**kwargs
23+
Keyword arguments are passed to the two Map objects.
24+
25+
Examples
26+
--------
27+
>>> # DualMap accepts the same arguments as Map:
28+
>>> m = DualMap(location=(0, 0), tiles='cartodbpositron', zoom_start=5)
29+
>>> # Add the same marker to both maps:
30+
>>> Marker((0, 0)).add_to(m)
31+
>>> # The individual maps are attributes called `m1` and `m2`:
32+
>>> Marker((0, 1)).add_to(m.m1)
33+
>>> LayerControl().add_to(m)
34+
>>> m.save('map.html')
35+
36+
"""
37+
38+
_template = Template("""
39+
{% macro script(this, kwargs) %}
40+
{{ this.m1.get_name() }}.sync({{ this.m2.get_name() }});
41+
{{ this.m2.get_name() }}.sync({{ this.m1.get_name() }});
42+
{% endmacro %}
43+
""")
44+
45+
def __init__(self, location=None, layout='horizontal', **kwargs):
46+
super(DualMap, self).__init__()
47+
for key in ('width', 'height', 'left', 'top', 'position'):
48+
assert key not in kwargs, ('Argument {} cannot be used with '
49+
'DualMap.'.format(key))
50+
if layout not in ('horizontal', 'vertical'):
51+
raise ValueError('Undefined option for argument `layout`: {}. '
52+
'Use either \'horizontal\' or \'vertical\'.'
53+
.format(layout))
54+
width = '50%' if layout == 'horizontal' else '100%'
55+
height = '100%' if layout == 'horizontal' else '50%'
56+
self.m1 = Map(location=location, width=width, height=height,
57+
left='0%', top='0%',
58+
position='absolute', **kwargs)
59+
self.m2 = Map(location=location, width=width, height=height,
60+
left='50%' if layout == 'horizontal' else '0%',
61+
top='0%' if layout == 'horizontal' else '50%',
62+
position='absolute', **kwargs)
63+
figure = Figure()
64+
figure.add_child(self.m1)
65+
figure.add_child(self.m2)
66+
# Important: add self to Figure last.
67+
figure.add_child(self)
68+
self.children_for_m2 = []
69+
self.children_for_m2_copied = [] # list with ids
70+
71+
def _repr_html_(self, **kwargs):
72+
"""Displays the HTML Map in a Jupyter notebook."""
73+
if self._parent is None:
74+
self.add_to(Figure())
75+
out = self._parent._repr_html_(**kwargs)
76+
self._parent = None
77+
else:
78+
out = self._parent._repr_html_(**kwargs)
79+
return out
80+
81+
def add_child(self, child, name=None, index=None):
82+
"""Add object `child` to the first map and store it for the second."""
83+
self.m1.add_child(child, name, index)
84+
if index is None:
85+
index = len(self.m2._children)
86+
self.children_for_m2.append((child, name, index))
87+
88+
def render(self, **kwargs):
89+
figure = self.get_root()
90+
assert isinstance(figure, Figure), ('You cannot render this Element '
91+
'if it is not in a Figure.')
92+
93+
figure.header.add_child(JavascriptLink('https://rawcdn.githack.com/jieter/Leaflet.Sync/master/L.Map.Sync.js'), # noqa
94+
name='Leaflet.Sync')
95+
96+
super(DualMap, self).render(**kwargs)
97+
98+
for child, name, index in self.children_for_m2:
99+
if child._id in self.children_for_m2_copied:
100+
# This map has been rendered before, child was copied already.
101+
continue
102+
child_copy = deep_copy(child)
103+
self.m2.add_child(child_copy, name, index)
104+
# m2 has already been rendered, so render the child here:
105+
child_copy.render()
106+
self.children_for_m2_copied.append(child._id)
107+
108+
def fit_bounds(self, *args, **kwargs):
109+
for m in (self.m1, self.m2):
110+
m.fit_bounds(*args, **kwargs)
111+
112+
def keep_in_front(self, *args):
113+
for m in (self.m1, self.m2):
114+
m.keep_in_front(*args)

folium/utilities.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import tempfile
1010
import zlib
1111
from contextlib import contextmanager
12+
import copy
13+
import uuid
14+
import collections
1215

1316
import numpy as np
1417

@@ -415,3 +418,17 @@ def _tmp_html(data):
415418
finally:
416419
if os.path.isfile(filepath):
417420
os.remove(filepath)
421+
422+
423+
def deep_copy(item_original):
424+
"""Return a recursive deep-copy of item where each copy has a new ID."""
425+
item = copy.copy(item_original)
426+
item._id = uuid.uuid4().hex
427+
if hasattr(item, '_children') and len(item._children) > 0:
428+
children_new = collections.OrderedDict()
429+
for subitem_original in item._children.values():
430+
subitem = deep_copy(subitem_original)
431+
subitem._parent = item
432+
children_new[subitem.get_name()] = subitem
433+
item._children = children_new
434+
return item

tests/plugins/test_dual_map.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Test DualMap
5+
------------
6+
"""
7+
8+
from __future__ import (absolute_import, division, print_function)
9+
10+
from jinja2 import Template
11+
12+
import folium
13+
import folium.plugins
14+
15+
16+
def test_dual_map():
17+
m = folium.plugins.DualMap((0, 0))
18+
19+
folium.FeatureGroup(name='both').add_to(m)
20+
folium.FeatureGroup(name='left').add_to(m.m1)
21+
folium.FeatureGroup(name='right').add_to(m.m2)
22+
23+
figure = m.get_root()
24+
assert isinstance(figure, folium.Figure)
25+
out = figure.render()
26+
27+
script = '<script src="https://rawcdn.githack.com/jieter/Leaflet.Sync/master/L.Map.Sync.js"></script>' # noqa
28+
assert script in out
29+
30+
tmpl = Template("""
31+
{{ this.m1.get_name() }}.sync({{ this.m2.get_name() }});
32+
{{ this.m2.get_name() }}.sync({{ this.m1.get_name() }});
33+
""")
34+
35+
assert tmpl.render(this=m) in out

tests/test_utilities.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
from __future__ import (absolute_import, division, print_function)
22

3-
from folium.utilities import camelize
3+
from folium.utilities import camelize, deep_copy
4+
from folium import Map, FeatureGroup, Marker
45

56

67
def test_camelize():
78
assert camelize('variable_name') == 'variableName'
89
assert camelize('variableName') == 'variableName'
910
assert camelize('name') == 'name'
1011
assert camelize('very_long_variable_name') == 'veryLongVariableName'
12+
13+
14+
def test_deep_copy():
15+
m = Map()
16+
fg = FeatureGroup().add_to(m)
17+
Marker(location=(0, 0)).add_to(fg)
18+
m_copy = deep_copy(m)
19+
20+
def check(item, item_copy):
21+
assert type(item) is type(item_copy)
22+
assert item._name == item_copy._name
23+
for attr in item.__dict__.keys():
24+
if not attr.startswith('_'):
25+
assert getattr(item, attr) == getattr(item_copy, attr)
26+
assert item is not item_copy
27+
assert item._id != item_copy._id
28+
for child, child_copy in zip(item._children.values(),
29+
item_copy._children.values()):
30+
check(child, child_copy)
31+
32+
check(m, m_copy)

0 commit comments

Comments
 (0)