|
| 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) |
0 commit comments