Skip to content

Commit

Permalink
feat: introduce leaflet-geoman as an alternative to leaflet-draw (#1181)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
iisakkirotko and maartenbreddels authored Apr 23, 2024
1 parent 4a732d0 commit 8a96abe
Show file tree
Hide file tree
Showing 12 changed files with 3,449 additions and 2,320 deletions.
2 changes: 1 addition & 1 deletion docs/controls/draw_control.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Draw Control
============

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

Example
-------
Expand Down
73 changes: 73 additions & 0 deletions docs/controls/geoman_draw_control.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Geoman Draw Control
============

``GeomanDrawControl`` allows one to draw various shapes on the map.
The drawing functionality on the front-end is provided by `geoman <https://geoman.io/>`_.

The following shapes are supported:
- marker
- circlemarker
- circle
- polyline
- rectangle
- polygon
- text

Additionally, there are modes that allow editing of previously drawn shapes:

- edit
- drag
- remove
- cut
- rotate

To have a drawing tool active on the map, pass it a non-empty dictionary with the desired options, see
`geoman documentation <https://www.geoman.io/docs/modes/draw-mode#customize-style>`_ for details.

Example
-------
.. jupyter-execute::

from ipyleaflet import Map, GeomanDrawControl

m = Map(center=(50, 354), zoom=5)

draw_control = GeomanDrawControl()
draw_control.polyline = {
"pathOptions": {
"color": "#6bc2e5",
"weight": 8,
"opacity": 1.0
}
}
draw_control.polygon = {
"pathOptions": {
"fillColor": "#6be5c3",
"color": "#6be5c3",
"fillOpacity": 1.0
}
}
draw_control.circlemarker = {
"pathOptions": {
"fillColor": "#efed69",
"color": "#efed69",
"fillOpacity": 0.62
}
}
draw_control.rectangle = {
"pathOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 1.0
}
}

m.add(draw_control)

m

Methods
-------

.. autoclass:: ipyleaflet.leaflet.GeomanDrawControl
:members:
171 changes: 141 additions & 30 deletions python/ipyleaflet_core/ipyleaflet/leaflet.py
Original file line number Diff line number Diff line change
Expand Up @@ -2127,24 +2127,7 @@ def _handle_leaflet_event(self, _, content, buffers):
self.x = event.x


class DrawControl(Control):
"""DrawControl class.
Drawing tools for drawing on the map.
"""

_view_name = Unicode("LeafletDrawControlView").tag(sync=True)
_model_name = Unicode("LeafletDrawControlModel").tag(sync=True)

# Enable each of the following drawing by giving them a non empty dict of options
# You can add Leaflet style options in the shapeOptions sub-dict
# See https://github.com/Leaflet/Leaflet.draw#polylineoptions
# TODO: mutable default value!
polyline = Dict({"shapeOptions": {}}).tag(sync=True)
# See https://github.com/Leaflet/Leaflet.draw#polygonoptions
# TODO: mutable default value!
polygon = Dict({"shapeOptions": {}}).tag(sync=True)
circlemarker = Dict({"shapeOptions": {}}).tag(sync=True)
class DrawControlBase(Control):

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

last_draw = Dict({"type": "Feature", "geometry": None})
last_action = Unicode()

_draw_callbacks = Instance(CallbackDispatcher, ())

def __init__(self, **kwargs):
super(DrawControl, self).__init__(**kwargs)
self.on_msg(self._handle_leaflet_event)

def _handle_leaflet_event(self, _, content, buffers):
if content.get("event", "").startswith("draw"):
event, action = content.get("event").split(":")
self.last_draw = content.get("geo_json")
self.last_action = action
self._draw_callbacks(self, action=action, geo_json=self.last_draw)
super(DrawControlBase, self).__init__(**kwargs)

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


class DrawControl(DrawControlBase):
"""DrawControl class.
Drawing tools for drawing on the map.
"""

_view_name = Unicode("LeafletDrawControlView").tag(sync=True)
_model_name = Unicode("LeafletDrawControlModel").tag(sync=True)

# Enable each of the following drawing by giving them a non empty dict of options
# You can add Leaflet style options in the shapeOptions sub-dict
# See https://github.com/Leaflet/Leaflet.draw#polylineoptions and
# https://github.com/Leaflet/Leaflet.draw#polygonoptions
polyline = Dict({ 'shapeOptions': {} }).tag(sync=True)
polygon = Dict({ 'shapeOptions': {} }).tag(sync=True)
circlemarker = Dict({ 'shapeOptions': {} }).tag(sync=True)

last_draw = Dict({"type": "Feature", "geometry": None})
last_action = Unicode()

def __init__(self, **kwargs):
super(DrawControl, self).__init__(**kwargs)
self.on_msg(self._handle_leaflet_event)

def _handle_leaflet_event(self, _, content, buffers):
if content.get("event", "").startswith("draw"):
event, action = content.get("event").split(":")
self.last_draw = content.get("geo_json")
self.last_action = action
self._draw_callbacks(self, action=action, geo_json=self.last_draw)


class GeomanDrawControl(DrawControlBase):
"""GeomanDrawControl class.
Alternative drawing tools for drawing on the map provided by Leaflet-Geoman.
"""

_view_name = Unicode("LeafletGeomanDrawControlView").tag(sync=True)
_model_name = Unicode("LeafletGeomanDrawControlModel").tag(sync=True)

# Current mode & shape
# valid values are: 'draw', 'edit', 'drag', 'remove', 'cut', 'rotate'
# for drawing, the tool can be added after ':' e.g. 'draw:marker'
current_mode = Any(allow_none=True, default_value=None).tag(sync=True)

# Hides toolbar
hide_controls = Bool(False).tag(sync=True)

# Different drawing modes
# See https://www.geoman.io/docs/modes/draw-mode
polyline = Dict({ 'pathOptions': {} }).tag(sync=True)
polygon = Dict({ 'pathOptions': {} }).tag(sync=True)
circlemarker = Dict({ 'pathOptions': {} }).tag(sync=True)

# Disabled by default
text = Dict().tag(sync=True)

# Tools
# See https://www.geoman.io/docs/modes
drag = Bool(True).tag(sync=True)
cut = Bool(True).tag(sync=True)
rotate = Bool(True).tag(sync=True)

def __init__(self, **kwargs):
super(GeomanDrawControl, self).__init__(**kwargs)
self.on_msg(self._handle_leaflet_event)

def _handle_leaflet_event(self, _, content, buffers):
if content.get('event', '').startswith('pm:'):
action = content.get('event').split(':')[1]
geo_json = content.get('geo_json')
if action == "vertexadded":
self._draw_callbacks(self, action=action, geo_json=geo_json)
return
# Some actions return only new feature, while others return all features
# in the layer
if not isinstance(geo_json, list):
geo_json = [geo_json]
self._draw_callbacks(self, action=action, geo_json=geo_json)

def on_draw(self, callback, remove=False):
"""Add a draw event listener.
Parameters
----------
callback : callable
Callback function that will be called on draw event.
remove: boolean
Whether to remove this callback or not. Defaults to False.
"""
self._draw_callbacks.register_callback(callback, remove=remove)

def clear_text(self):
"""Clear all text."""
self.send({'msg': 'clear_text'})


class DrawControlCompatibility(DrawControlBase):
"""DrawControl class.
Python side compatibility layer for old DrawControls, using the new Geoman front-end but old Python API.
"""

_view_name = Unicode("LeafletGeomanDrawControlView").tag(sync=True)
_model_name = Unicode("LeafletGeomanDrawControlModel").tag(sync=True)

# Different drawing modes
# See https://www.geoman.io/docs/modes/draw-mode
polyline = Dict({ 'shapeOptions': {} }).tag(sync=True)
polygon = Dict({ 'shapeOptions': {} }).tag(sync=True)
circlemarker = Dict({ 'shapeOptions': {} }).tag(sync=True)

last_draw = Dict({
'type': 'Feature',
'geometry': None
})
last_action = Unicode()

def __init__(self, **kwargs):
super(DrawControlCompatibility, self).__init__(**kwargs)
self.on_msg(self._handle_leaflet_event)

def _handle_leaflet_event(self, _, content, buffers):
if content.get('event', '').startswith('pm:'):
action = content.get('event').split(':')[1]
geo_json = content.get('geo_json')
# We remove vertexadded events, since they were not available through leaflet-draw
if action == "vertexadded":
return
# Some actions return only new feature, while others return all features
# in the layer
if not isinstance(geo_json, dict):
geo_json = geo_json[-1]
self.last_draw = geo_json
self.last_action = action
self._draw_callbacks(self, action=action, geo_json=self.last_draw)


class ZoomControl(Control):
"""ZoomControl class, with Control as parent class.
Expand Down
1 change: 1 addition & 0 deletions python/jupyter_leaflet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"watch:nbextension": "webpack --watch"
},
"dependencies": {
"@geoman-io/leaflet-geoman-free": "^2.16.0",
"@jupyter-widgets/base": "^2 || ^3 || ^4 || ^5 || ^6",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
Expand Down
11 changes: 11 additions & 0 deletions python/jupyter_leaflet/src/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
LeafletControlView,
LeafletLayerModel,
LeafletLayerView,
LeafletGeomanDrawControlView,
} from './jupyter-leaflet';
import L from './leaflet';
import { getProjection } from './projections';
Expand Down Expand Up @@ -234,6 +235,16 @@ export class LeafletMapView extends LeafletDOMWidgetView {
const view = await this.create_child_view<LeafletControlView>(child_model, {
map_view: this,
});
// Work around for Geoman creating and adding its own toolbar
// TODO: remove the special case
if (
view instanceof LeafletGeomanDrawControlView &&
!child_model.get('hide_controls')
) {
this.obj.pm.addControls(view.controlOptions);
return view;
}

this.obj.addControl(view.obj);
// Trigger the displayed event of the child view.
this.displayed.then(() => {
Expand Down
Loading

0 comments on commit 8a96abe

Please sign in to comment.