Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce leaflet-geoman as an alternative to leaflet-draw #1181

Merged
merged 9 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 &&
martinRenou marked this conversation as resolved.
Show resolved Hide resolved
!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
Loading