Skip to content

Commit 3970364

Browse files
authored
Fix geoman controls option update + snapping option (#1247)
* Add pm_ignore to TypeScript side interface definitions, and add get_options to GeoJson constructor to fetch all synced values. * Add super.model_events call to geoJson * bump version of geoman * Add snap ignore option to layers and geoman control * Split definition of polygon and polyline views to appease TypeScript. * docs * Make the geomancontrol match the Control interface so that it can be safely removed without triggering an exception. * Force the geoman toolbar to update on options change by removing and re-adding it in case of options changing. * Remove needles log * FIX: get() call should be on model, not options; replace special case for geoman controls with implementation of Control IFace within the Geoman View via addTo * FIX: to implement the Control interface, setting the position should use the same options setting method as when options dynamically change. * Avoid listening to current mode on change. * Lint * FIX: bugs in pointToLayer method prevented providing existing Points to .data attr for editing. Also made circlemarker style use the draw control configured style if the feature doesn't have its own style overrides. * Fix: remove listeners before removing controls, since doing it the other way around seems to break rendering. * Add pm_ignore to TypeScript side interface definitions, and add get_options to GeoJson constructor to fetch all synced values. * Add super.model_events call to geoJson * bump version of geoman * Add snap ignore option to layers and geoman control * Split definition of polygon and polyline views to appease TypeScript. * docs * Make the geomancontrol match the Control interface so that it can be safely removed without triggering an exception. * Force the geoman toolbar to update on options change by removing and re-adding it in case of options changing. * Remove needles log * FIX: get() call should be on model, not options; replace special case for geoman controls with implementation of Control IFace within the Geoman View via addTo * FIX: to implement the Control interface, setting the position should use the same options setting method as when options dynamically change. * Avoid listening to current mode on change. * Lint * FIX: bugs in pointToLayer method prevented providing existing Points to .data attr for editing. Also made circlemarker style use the draw control configured style if the feature doesn't have its own style overrides. * Fix: remove listeners before removing controls, since doing it the other way around seems to break rendering. * Cleanup GeomanDrawControl.ipynb
1 parent 747e29e commit 3970364

File tree

10 files changed

+3574
-2948
lines changed

10 files changed

+3574
-2948
lines changed

examples/GeomanDrawControl.ipynb

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"metadata": {
7+
"ExecuteTime": {
8+
"end_time": "2025-06-09T15:35:39.559258Z",
9+
"start_time": "2025-06-09T15:35:39.448345Z"
10+
}
11+
},
12+
"outputs": [],
13+
"source": [
14+
"from ipyleaflet import (\n",
15+
" Map,\n",
16+
" Marker,\n",
17+
" TileLayer,\n",
18+
" ImageOverlay,\n",
19+
" Polyline,\n",
20+
" Polygon,\n",
21+
" Rectangle,\n",
22+
" Circle,\n",
23+
" CircleMarker,\n",
24+
" GeoJSON,\n",
25+
" GeomanDrawControl,\n",
26+
")\n",
27+
"\n",
28+
"from traitlets import link"
29+
]
30+
},
31+
{
32+
"cell_type": "code",
33+
"execution_count": 2,
34+
"metadata": {
35+
"ExecuteTime": {
36+
"end_time": "2025-05-15T18:21:31.116235Z",
37+
"start_time": "2025-05-15T18:21:31.114028Z"
38+
}
39+
},
40+
"outputs": [],
41+
"source": [
42+
"import json\n",
43+
"\n",
44+
"with open(\"simple.geo.json\") as f:\n",
45+
" data = json.load(f)"
46+
]
47+
},
48+
{
49+
"cell_type": "code",
50+
"execution_count": 3,
51+
"metadata": {
52+
"ExecuteTime": {
53+
"end_time": "2025-05-15T18:21:31.197905Z",
54+
"start_time": "2025-05-15T18:21:31.195304Z"
55+
}
56+
},
57+
"outputs": [],
58+
"source": [
59+
"d1 = data.copy()\n",
60+
"d2 = data.copy()\n",
61+
"\n",
62+
"d1[\"features\"] = d1[\"features\"][:1]\n",
63+
"d2[\"features\"] = d2[\"features\"][1:]\n"
64+
]
65+
},
66+
{
67+
"cell_type": "code",
68+
"execution_count": 12,
69+
"metadata": {
70+
"ExecuteTime": {
71+
"end_time": "2025-05-15T18:21:36.974937Z",
72+
"start_time": "2025-05-15T18:21:36.966638Z"
73+
}
74+
},
75+
"outputs": [
76+
{
77+
"data": {
78+
"application/vnd.jupyter.widget-view+json": {
79+
"model_id": "14649585f32d4174a701bdd9d77a8b6a",
80+
"version_major": 2,
81+
"version_minor": 0
82+
},
83+
"text/plain": [
84+
"Map(center=[46.475212657477016, 6.3198722284199675], controls=(ZoomControl(options=['position', 'zoom_in_text'…"
85+
]
86+
},
87+
"execution_count": 12,
88+
"metadata": {},
89+
"output_type": "execute_result"
90+
}
91+
],
92+
"source": [
93+
"center = [46.475212657477016, 6.3198722284199675]\n",
94+
"zoom = 9\n",
95+
"m = Map(center=center, zoom=zoom, layout=dict(height=\"600px\"))\n",
96+
"\n",
97+
"g1 = GeoJSON(data=d1, pm_ignore=True, snap_ignore=True, \n",
98+
" style={\n",
99+
" 'opacity': 1, 'dashArray': '9', 'fillOpacity': 0.1, 'weight': 1\n",
100+
" },\n",
101+
" )\n",
102+
"\n",
103+
"g2 = GeoJSON(data=d2, pm_ignore=True, snap_ignore=False)\n",
104+
"m.add(g1)\n",
105+
"m.add(g2)\n",
106+
"\n",
107+
"draw_data = data[\"features\"][1]\n",
108+
"draw_data[\"properties\"][\"type\"] = \"circlemarker\"\n",
109+
"dc = GeomanDrawControl(\n",
110+
" marker={},\n",
111+
" circlemarker={},\n",
112+
" polygon={},\n",
113+
" data=[draw_data]\n",
114+
")\n",
115+
"\n",
116+
"def handle_draw(target, action, geo_json):\n",
117+
" print(action)\n",
118+
" print(geo_json)\n",
119+
"\n",
120+
"\n",
121+
"dc.on_draw(handle_draw)\n",
122+
"m.add(dc)\n",
123+
"m"
124+
]
125+
},
126+
{
127+
"cell_type": "code",
128+
"execution_count": 13,
129+
"metadata": {},
130+
"outputs": [
131+
{
132+
"data": {
133+
"application/vnd.jupyter.widget-view+json": {
134+
"model_id": "14649585f32d4174a701bdd9d77a8b6a",
135+
"version_major": 2,
136+
"version_minor": 0
137+
},
138+
"text/plain": [
139+
"Map(bottom=46680.0, center=[46.475212657477016, 6.3198722284199675], controls=(ZoomControl(options=['position'…"
140+
]
141+
},
142+
"execution_count": 13,
143+
"metadata": {},
144+
"output_type": "execute_result"
145+
}
146+
],
147+
"source": [
148+
"# Modifying draw options will update the toolbar\n",
149+
"dc.marker={\"markerStyle\": {\"color\": \"#FF0000\"}}\n",
150+
"dc.rectangle={\"pathOptions\": {\"color\": \"#FF0000\"}}\n",
151+
"dc.circlemarker={\"pathOptions\": {\"color\": \"#FF0000\"}}\n",
152+
"m"
153+
]
154+
},
155+
{
156+
"cell_type": "code",
157+
"execution_count": null,
158+
"metadata": {},
159+
"outputs": [],
160+
"source": []
161+
}
162+
],
163+
"metadata": {
164+
"kernelspec": {
165+
"display_name": "Python 3 (ipykernel)",
166+
"language": "python",
167+
"name": "python3"
168+
},
169+
"language_info": {
170+
"codemirror_mode": {
171+
"name": "ipython",
172+
"version": 3
173+
},
174+
"file_extension": ".py",
175+
"mimetype": "text/x-python",
176+
"name": "python",
177+
"nbconvert_exporter": "python",
178+
"pygments_lexer": "ipython3",
179+
"version": "3.11.12"
180+
}
181+
},
182+
"nbformat": 4,
183+
"nbformat_minor": 4
184+
}

python/ipyleaflet/ipyleaflet/leaflet.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ class Layer(Widget, InteractMixin):
173173
Name of the pane to use for the layer.
174174
pm_ignore: boolean
175175
Make Leaflet-Geoman ignore the layer, so it cannot modify it.
176+
snap_ignore: boolean
177+
Make Leaflet-Geoman snapping ignore the layer, so it is not used as a snap target when editing.
176178
"""
177179

178180
_view_name = Unicode("LeafletLayerView").tag(sync=True)
@@ -198,6 +200,7 @@ class Layer(Widget, InteractMixin):
198200
subitems = Tuple().tag(trait=Instance(Widget), sync=True, **widget_serialization)
199201

200202
pm_ignore = Bool(True).tag(sync=True, o=True)
203+
snap_ignore = Bool(True).tag(sync=True, o=False)
201204

202205
@validate("subitems")
203206
def _validate_subitems(self, proposal):

python/jupyter_leaflet/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"watch:nbextension": "webpack --watch"
4141
},
4242
"dependencies": {
43-
"@geoman-io/leaflet-geoman-free": "^2.16.0",
43+
"@geoman-io/leaflet-geoman-free": "^2.18.0",
4444
"@jupyter-widgets/base": "^2 || ^3 || ^4 || ^5 || ^6",
4545
"buffer": "^6.0.3",
4646
"crypto-browserify": "^3.12.0",

python/jupyter_leaflet/src/Map.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -227,23 +227,14 @@ export class LeafletMapView extends LeafletDOMWidgetView {
227227
}
228228

229229
remove_control_view(child_view: LeafletControlView) {
230-
this.obj.removeControl(child_view.obj);
231230
child_view.remove();
231+
this.obj.removeControl(child_view.obj);
232232
}
233233

234234
async add_control_model(child_model: LeafletControlModel) {
235235
const view = await this.create_child_view<LeafletControlView>(child_model, {
236236
map_view: this,
237237
});
238-
// Work around for Geoman creating and adding its own toolbar
239-
// TODO: remove the special case
240-
if (
241-
view instanceof LeafletGeomanDrawControlView &&
242-
!child_model.get('hide_controls')
243-
) {
244-
this.obj.pm.addControls(view.controlOptions);
245-
return view;
246-
}
247238

248239
this.obj.addControl(view.obj);
249240
// Trigger the displayed event of the child view.

0 commit comments

Comments
 (0)