Skip to content

Commit 8a96abe

Browse files
feat: introduce leaflet-geoman as an alternative to leaflet-draw (#1181)
* feat: move from leaflet-draw to leaflet-geoman * refactor: improved backwards compatibility * docs: document GeomanDrawControl * implement fixes to issues found in review * refactor: retain old DrawControl for migration period * update reference images * incorporate suggestions from review * Apply suggestions from code review Co-authored-by: Maarten Breddels <[email protected]> * fix: correct default options previously `circlemarker` incorrectly defaulted to using `markerStyle`, while it isn't a leaflet marker layer. Moreover, polyline and polygon would use the old (`shapeOptions`) by default even with geoman, where it should be `pathOptions`. This was fixed by the compatibility layer, but this way the defaults are clearer. --------- Co-authored-by: Maarten Breddels <[email protected]>
1 parent 4a732d0 commit 8a96abe

File tree

12 files changed

+3449
-2320
lines changed

12 files changed

+3449
-2320
lines changed

docs/controls/draw_control.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Draw Control
22
============
33

4-
The ``DrawControl`` allows one to draw shapes on the map such as ``Rectangle`` ``Circle`` or lines.
4+
The ``DrawControl`` is deprecated and will be removed in a future release. Please use ``GeomanDrawControl`` instead.
55

66
Example
77
-------

docs/controls/geoman_draw_control.rst

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Geoman Draw Control
2+
============
3+
4+
``GeomanDrawControl`` allows one to draw various shapes on the map.
5+
The drawing functionality on the front-end is provided by `geoman <https://geoman.io/>`_.
6+
7+
The following shapes are supported:
8+
- marker
9+
- circlemarker
10+
- circle
11+
- polyline
12+
- rectangle
13+
- polygon
14+
- text
15+
16+
Additionally, there are modes that allow editing of previously drawn shapes:
17+
18+
- edit
19+
- drag
20+
- remove
21+
- cut
22+
- rotate
23+
24+
To have a drawing tool active on the map, pass it a non-empty dictionary with the desired options, see
25+
`geoman documentation <https://www.geoman.io/docs/modes/draw-mode#customize-style>`_ for details.
26+
27+
Example
28+
-------
29+
.. jupyter-execute::
30+
31+
from ipyleaflet import Map, GeomanDrawControl
32+
33+
m = Map(center=(50, 354), zoom=5)
34+
35+
draw_control = GeomanDrawControl()
36+
draw_control.polyline = {
37+
"pathOptions": {
38+
"color": "#6bc2e5",
39+
"weight": 8,
40+
"opacity": 1.0
41+
}
42+
}
43+
draw_control.polygon = {
44+
"pathOptions": {
45+
"fillColor": "#6be5c3",
46+
"color": "#6be5c3",
47+
"fillOpacity": 1.0
48+
}
49+
}
50+
draw_control.circlemarker = {
51+
"pathOptions": {
52+
"fillColor": "#efed69",
53+
"color": "#efed69",
54+
"fillOpacity": 0.62
55+
}
56+
}
57+
draw_control.rectangle = {
58+
"pathOptions": {
59+
"fillColor": "#fca45d",
60+
"color": "#fca45d",
61+
"fillOpacity": 1.0
62+
}
63+
}
64+
65+
m.add(draw_control)
66+
67+
m
68+
69+
Methods
70+
-------
71+
72+
.. autoclass:: ipyleaflet.leaflet.GeomanDrawControl
73+
:members:

python/ipyleaflet_core/ipyleaflet/leaflet.py

Lines changed: 141 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,24 +2127,7 @@ def _handle_leaflet_event(self, _, content, buffers):
21272127
self.x = event.x
21282128

21292129

2130-
class DrawControl(Control):
2131-
"""DrawControl class.
2132-
2133-
Drawing tools for drawing on the map.
2134-
"""
2135-
2136-
_view_name = Unicode("LeafletDrawControlView").tag(sync=True)
2137-
_model_name = Unicode("LeafletDrawControlModel").tag(sync=True)
2138-
2139-
# Enable each of the following drawing by giving them a non empty dict of options
2140-
# You can add Leaflet style options in the shapeOptions sub-dict
2141-
# See https://github.com/Leaflet/Leaflet.draw#polylineoptions
2142-
# TODO: mutable default value!
2143-
polyline = Dict({"shapeOptions": {}}).tag(sync=True)
2144-
# See https://github.com/Leaflet/Leaflet.draw#polygonoptions
2145-
# TODO: mutable default value!
2146-
polygon = Dict({"shapeOptions": {}}).tag(sync=True)
2147-
circlemarker = Dict({"shapeOptions": {}}).tag(sync=True)
2130+
class DrawControlBase(Control):
21482131

21492132
# Leave empty to disable these
21502133
circle = Dict().tag(sync=True)
@@ -2158,21 +2141,10 @@ class DrawControl(Control):
21582141
# Layer data
21592142
data = List().tag(sync=True)
21602143

2161-
last_draw = Dict({"type": "Feature", "geometry": None})
2162-
last_action = Unicode()
2163-
21642144
_draw_callbacks = Instance(CallbackDispatcher, ())
21652145

21662146
def __init__(self, **kwargs):
2167-
super(DrawControl, self).__init__(**kwargs)
2168-
self.on_msg(self._handle_leaflet_event)
2169-
2170-
def _handle_leaflet_event(self, _, content, buffers):
2171-
if content.get("event", "").startswith("draw"):
2172-
event, action = content.get("event").split(":")
2173-
self.last_draw = content.get("geo_json")
2174-
self.last_action = action
2175-
self._draw_callbacks(self, action=action, geo_json=self.last_draw)
2147+
super(DrawControlBase, self).__init__(**kwargs)
21762148

21772149
def on_draw(self, callback, remove=False):
21782150
"""Add a draw event listener.
@@ -2215,6 +2187,145 @@ def clear_markers(self):
22152187
self.send({"msg": "clear_markers"})
22162188

22172189

2190+
class DrawControl(DrawControlBase):
2191+
"""DrawControl class.
2192+
2193+
Drawing tools for drawing on the map.
2194+
"""
2195+
2196+
_view_name = Unicode("LeafletDrawControlView").tag(sync=True)
2197+
_model_name = Unicode("LeafletDrawControlModel").tag(sync=True)
2198+
2199+
# Enable each of the following drawing by giving them a non empty dict of options
2200+
# You can add Leaflet style options in the shapeOptions sub-dict
2201+
# See https://github.com/Leaflet/Leaflet.draw#polylineoptions and
2202+
# https://github.com/Leaflet/Leaflet.draw#polygonoptions
2203+
polyline = Dict({ 'shapeOptions': {} }).tag(sync=True)
2204+
polygon = Dict({ 'shapeOptions': {} }).tag(sync=True)
2205+
circlemarker = Dict({ 'shapeOptions': {} }).tag(sync=True)
2206+
2207+
last_draw = Dict({"type": "Feature", "geometry": None})
2208+
last_action = Unicode()
2209+
2210+
def __init__(self, **kwargs):
2211+
super(DrawControl, self).__init__(**kwargs)
2212+
self.on_msg(self._handle_leaflet_event)
2213+
2214+
def _handle_leaflet_event(self, _, content, buffers):
2215+
if content.get("event", "").startswith("draw"):
2216+
event, action = content.get("event").split(":")
2217+
self.last_draw = content.get("geo_json")
2218+
self.last_action = action
2219+
self._draw_callbacks(self, action=action, geo_json=self.last_draw)
2220+
2221+
2222+
class GeomanDrawControl(DrawControlBase):
2223+
"""GeomanDrawControl class.
2224+
2225+
Alternative drawing tools for drawing on the map provided by Leaflet-Geoman.
2226+
"""
2227+
2228+
_view_name = Unicode("LeafletGeomanDrawControlView").tag(sync=True)
2229+
_model_name = Unicode("LeafletGeomanDrawControlModel").tag(sync=True)
2230+
2231+
# Current mode & shape
2232+
# valid values are: 'draw', 'edit', 'drag', 'remove', 'cut', 'rotate'
2233+
# for drawing, the tool can be added after ':' e.g. 'draw:marker'
2234+
current_mode = Any(allow_none=True, default_value=None).tag(sync=True)
2235+
2236+
# Hides toolbar
2237+
hide_controls = Bool(False).tag(sync=True)
2238+
2239+
# Different drawing modes
2240+
# See https://www.geoman.io/docs/modes/draw-mode
2241+
polyline = Dict({ 'pathOptions': {} }).tag(sync=True)
2242+
polygon = Dict({ 'pathOptions': {} }).tag(sync=True)
2243+
circlemarker = Dict({ 'pathOptions': {} }).tag(sync=True)
2244+
2245+
# Disabled by default
2246+
text = Dict().tag(sync=True)
2247+
2248+
# Tools
2249+
# See https://www.geoman.io/docs/modes
2250+
drag = Bool(True).tag(sync=True)
2251+
cut = Bool(True).tag(sync=True)
2252+
rotate = Bool(True).tag(sync=True)
2253+
2254+
def __init__(self, **kwargs):
2255+
super(GeomanDrawControl, self).__init__(**kwargs)
2256+
self.on_msg(self._handle_leaflet_event)
2257+
2258+
def _handle_leaflet_event(self, _, content, buffers):
2259+
if content.get('event', '').startswith('pm:'):
2260+
action = content.get('event').split(':')[1]
2261+
geo_json = content.get('geo_json')
2262+
if action == "vertexadded":
2263+
self._draw_callbacks(self, action=action, geo_json=geo_json)
2264+
return
2265+
# Some actions return only new feature, while others return all features
2266+
# in the layer
2267+
if not isinstance(geo_json, list):
2268+
geo_json = [geo_json]
2269+
self._draw_callbacks(self, action=action, geo_json=geo_json)
2270+
2271+
def on_draw(self, callback, remove=False):
2272+
"""Add a draw event listener.
2273+
2274+
Parameters
2275+
----------
2276+
callback : callable
2277+
Callback function that will be called on draw event.
2278+
remove: boolean
2279+
Whether to remove this callback or not. Defaults to False.
2280+
"""
2281+
self._draw_callbacks.register_callback(callback, remove=remove)
2282+
2283+
def clear_text(self):
2284+
"""Clear all text."""
2285+
self.send({'msg': 'clear_text'})
2286+
2287+
2288+
class DrawControlCompatibility(DrawControlBase):
2289+
"""DrawControl class.
2290+
2291+
Python side compatibility layer for old DrawControls, using the new Geoman front-end but old Python API.
2292+
"""
2293+
2294+
_view_name = Unicode("LeafletGeomanDrawControlView").tag(sync=True)
2295+
_model_name = Unicode("LeafletGeomanDrawControlModel").tag(sync=True)
2296+
2297+
# Different drawing modes
2298+
# See https://www.geoman.io/docs/modes/draw-mode
2299+
polyline = Dict({ 'shapeOptions': {} }).tag(sync=True)
2300+
polygon = Dict({ 'shapeOptions': {} }).tag(sync=True)
2301+
circlemarker = Dict({ 'shapeOptions': {} }).tag(sync=True)
2302+
2303+
last_draw = Dict({
2304+
'type': 'Feature',
2305+
'geometry': None
2306+
})
2307+
last_action = Unicode()
2308+
2309+
def __init__(self, **kwargs):
2310+
super(DrawControlCompatibility, self).__init__(**kwargs)
2311+
self.on_msg(self._handle_leaflet_event)
2312+
2313+
def _handle_leaflet_event(self, _, content, buffers):
2314+
if content.get('event', '').startswith('pm:'):
2315+
action = content.get('event').split(':')[1]
2316+
geo_json = content.get('geo_json')
2317+
# We remove vertexadded events, since they were not available through leaflet-draw
2318+
if action == "vertexadded":
2319+
return
2320+
# Some actions return only new feature, while others return all features
2321+
# in the layer
2322+
if not isinstance(geo_json, dict):
2323+
geo_json = geo_json[-1]
2324+
self.last_draw = geo_json
2325+
self.last_action = action
2326+
self._draw_callbacks(self, action=action, geo_json=self.last_draw)
2327+
2328+
22182329
class ZoomControl(Control):
22192330
"""ZoomControl class, with Control as parent class.
22202331

python/jupyter_leaflet/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"watch:nbextension": "webpack --watch"
4040
},
4141
"dependencies": {
42+
"@geoman-io/leaflet-geoman-free": "^2.16.0",
4243
"@jupyter-widgets/base": "^2 || ^3 || ^4 || ^5 || ^6",
4344
"buffer": "^6.0.3",
4445
"crypto-browserify": "^3.12.0",

python/jupyter_leaflet/src/Map.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
LeafletControlView,
2121
LeafletLayerModel,
2222
LeafletLayerView,
23+
LeafletGeomanDrawControlView,
2324
} from './jupyter-leaflet';
2425
import L from './leaflet';
2526
import { getProjection } from './projections';
@@ -234,6 +235,16 @@ export class LeafletMapView extends LeafletDOMWidgetView {
234235
const view = await this.create_child_view<LeafletControlView>(child_model, {
235236
map_view: this,
236237
});
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+
}
247+
237248
this.obj.addControl(view.obj);
238249
// Trigger the displayed event of the child view.
239250
this.displayed.then(() => {

0 commit comments

Comments
 (0)