Skip to content

Commit 451b851

Browse files
authored
Merge pull request #999 from davidbrochart/panes
Add Map panes
2 parents 183bf8f + 884bd8c commit 451b851

File tree

4 files changed

+174
-2
lines changed

4 files changed

+174
-2
lines changed

examples/MapPanes.ipynb

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "cbcccab4-2d48-4ed2-9a01-3aebef7a3f2e",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"import os\n",
11+
"import json\n",
12+
"import requests\n",
13+
"from ipyleaflet import Map, GeoJSON, Heatmap"
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"id": "d6a05f27-4740-4561-9164-fbf7cdd627a3",
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"if not os.path.exists('europe_110.geo.json'):\n",
24+
" url = 'https://github.com/jupyter-widgets/ipyleaflet/raw/master/examples/europe_110.geo.json'\n",
25+
" r = requests.get(url)\n",
26+
" with open('europe_110.geo.json', 'w') as f:\n",
27+
" f.write(r.content.decode(\"utf-8\"))\n",
28+
"\n",
29+
"with open('europe_110.geo.json', 'r') as f:\n",
30+
" data = json.load(f)\n",
31+
"\n",
32+
"data['features'] = [data['features'][0]] # Trim to one country so printing layers is readable\n",
33+
"\n",
34+
"m = Map(center=(40.9964, 19.9851), zoom=6, panes={\"heatmap_down\": {\"zIndex\": 350}, \"heatmap_top\": {\"zIndex\": 650}})\n",
35+
"\n",
36+
"geo_json = GeoJSON(\n",
37+
" data=data,\n",
38+
" style={\n",
39+
" 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.9, 'weight': 1, 'fillColor': 'white',\n",
40+
" },\n",
41+
")\n",
42+
"\n",
43+
"heatmap = Heatmap(\n",
44+
" locations=[[41.327,19.819],[40.73,19.562]],\n",
45+
" radius=5,\n",
46+
" blur=1,\n",
47+
" min_opacity=1,\n",
48+
")\n",
49+
"\n",
50+
"m.add_layer(heatmap)\n",
51+
"\n",
52+
"m.add_layer(geo_json)"
53+
]
54+
},
55+
{
56+
"cell_type": "code",
57+
"execution_count": null,
58+
"id": "7ae14f1e-203a-4622-8d6f-7f112d28c63c",
59+
"metadata": {},
60+
"outputs": [],
61+
"source": [
62+
"m"
63+
]
64+
},
65+
{
66+
"cell_type": "code",
67+
"execution_count": null,
68+
"id": "4ce8c2df-156c-40d0-9123-b1e905c13b03",
69+
"metadata": {},
70+
"outputs": [],
71+
"source": [
72+
"heatmap.pane = \"heatmap_top\""
73+
]
74+
},
75+
{
76+
"cell_type": "code",
77+
"execution_count": null,
78+
"id": "bb558261-3bdf-45d9-964a-52314fdf87cb",
79+
"metadata": {},
80+
"outputs": [],
81+
"source": [
82+
"heatmap.pane = \"heatmap_down\""
83+
]
84+
}
85+
],
86+
"metadata": {
87+
"kernelspec": {
88+
"display_name": "Python 3 (ipykernel)",
89+
"language": "python",
90+
"name": "python3"
91+
},
92+
"language_info": {
93+
"codemirror_mode": {
94+
"name": "ipython",
95+
"version": 3
96+
},
97+
"file_extension": ".py",
98+
"mimetype": "text/x-python",
99+
"name": "python",
100+
"nbconvert_exporter": "python",
101+
"pygments_lexer": "ipython3",
102+
"version": "3.10.5"
103+
}
104+
},
105+
"nbformat": 4,
106+
"nbformat_minor": 5
107+
}

ipyleaflet/leaflet.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ def get_value(change):
8080
return future
8181

8282

83+
class PaneException(TraitError):
84+
"""Custom PaneException class."""
85+
pass
86+
87+
8388
class LayerException(TraitError):
8489
"""Custom LayerException class."""
8590
pass
@@ -112,6 +117,8 @@ class Layer(Widget, InteractMixin):
112117
Custom name for the layer, which will be used by the LayersControl.
113118
popup: object
114119
Interactive widget that will be shown in a Popup when clicking on the layer.
120+
pane: string
121+
Name of the pane to use for the layer.
115122
"""
116123

117124
_view_name = Unicode('LeafletLayerView').tag(sync=True)
@@ -129,6 +136,7 @@ class Layer(Widget, InteractMixin):
129136
popup_min_width = Int(50).tag(sync=True)
130137
popup_max_width = Int(300).tag(sync=True)
131138
popup_max_height = Int(default_value=None, allow_none=True).tag(sync=True)
139+
pane = Unicode('').tag(sync=True)
132140

133141
options = List(trait=Unicode()).tag(sync=True)
134142

@@ -2181,6 +2189,7 @@ def _default_options(self):
21812189
right = Float(0, read_only=True).tag(sync=True)
21822190
left = Float(9007199254740991, read_only=True).tag(sync=True)
21832191

2192+
panes = Dict().tag(sync=True)
21842193
layers = Tuple().tag(trait=Instance(Layer), sync=True, **widget_serialization)
21852194

21862195
@default('layers')
@@ -2243,6 +2252,19 @@ def observe_attribution_control(self, change):
22432252
if self.attribution_control_instance is not None and self.attribution_control_instance in self.controls:
22442253
self.remove(self.attribution_control_instance)
22452254

2255+
@validate('panes')
2256+
def _validate_panes(self, proposal):
2257+
'''Validate panes.
2258+
'''
2259+
error_msg = "Panes should look like: {'pane_name': {'zIndex': 650, 'pointerEvents': 'none'}, ...}"
2260+
for k1, v1 in proposal.value.items():
2261+
if not isinstance(k1, str) or not isinstance(v1, dict):
2262+
raise PaneException(error_msg)
2263+
for k2, v2 in v1.items():
2264+
if not isinstance(k2, str) or not isinstance(v2, (str, int, float)):
2265+
raise PaneException(error_msg)
2266+
return proposal.value
2267+
22462268
_layer_ids = List()
22472269

22482270
@validate('layers')

js/src/Map.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class LeafletMapModel extends widgets.DOMWidgetModel {
7373
right: 0,
7474
left: 9007199254740991,
7575
options: [],
76+
panes: {},
7677
layers: [],
7778
controls: [],
7879
crs: {
@@ -165,6 +166,17 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
165166
this.dirty = false;
166167
}
167168

169+
create_panes() {
170+
const panes = this.model.get('panes');
171+
for (const name in panes) {
172+
const pane = this.obj.createPane(name);
173+
const styles = panes[name];
174+
for (const key in styles) {
175+
pane.style[key] = styles[key];
176+
}
177+
}
178+
}
179+
168180
remove_layer_view(child_view) {
169181
this.obj.removeLayer(child_view.obj);
170182
child_view.remove();
@@ -210,7 +222,7 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
210222
this.el.classList.add('jupyter-widgets');
211223
this.el.classList.add('leaflet-widgets');
212224
this.map_container = document.createElement('div');
213-
this.el.appendChild(this.map_container);
225+
this.map_child = this.el.appendChild(this.map_container);
214226
if (this.get_options().interpolation == 'nearest') {
215227
this.map_container.classList.add('crisp-image');
216228
}
@@ -229,6 +241,7 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
229241

230242
render_leaflet() {
231243
this.create_obj().then(() => {
244+
this.create_panes();
232245
this.layer_views.update(this.model.get('layers'));
233246
this.control_views.update(this.model.get('controls'));
234247
this.leaflet_events();
@@ -253,6 +266,13 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
253266
});
254267
}
255268

269+
rerender() {
270+
this.obj.remove();
271+
delete this.obj;
272+
this.el.removeChild(this.map_child);
273+
this.render();
274+
}
275+
256276
leaflet_events() {
257277
this.obj.on('moveend', e => {
258278
if (!this.dirty) {
@@ -317,6 +337,12 @@ export class LeafletMapView extends utils.LeafletDOMWidgetView {
317337
);
318338
}
319339
this.listenTo(this.model, 'msg:custom', this.handle_msg, this);
340+
this.listenTo(
341+
this.model,
342+
'change:panes',
343+
this.rerender,
344+
this
345+
);
320346
this.listenTo(
321347
this.model,
322348
'change:layers',

js/src/layers/Layer.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export class LeafletLayerModel extends widgets.WidgetModel {
2323
popup: null,
2424
popup_min_width: 50,
2525
popup_max_width: 300,
26-
popup_max_height: null
26+
popup_max_height: null,
27+
pane: ''
2728
};
2829
}
2930
}
@@ -58,9 +59,17 @@ export class LeafletLayerView extends utils.LeafletWidgetView {
5859
this.listenTo(this.model, 'change:popup', function(model, value) {
5960
this.bind_popup(value);
6061
});
62+
this.update_pane();
6163
});
6264
}
6365

66+
update_pane() {
67+
const pane = this.model.get('pane');
68+
if (pane !== '') {
69+
L.setOptions(this.obj, {pane});
70+
}
71+
}
72+
6473
leaflet_events() {
6574
// If the layer is interactive
6675
if (this.obj.on) {
@@ -111,6 +120,14 @@ export class LeafletLayerView extends utils.LeafletWidgetView {
111120
this.update_popup,
112121
this
113122
);
123+
this.listenTo(
124+
this.model,
125+
'change:pane',
126+
function() {
127+
this.map_view.rerender();
128+
},
129+
this
130+
);
114131
}
115132

116133
remove() {

0 commit comments

Comments
 (0)