Skip to content

Feat continuous update draw control option #1266

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
57 changes: 28 additions & 29 deletions examples/GeomanDrawControl.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"outputs": [],
"source": [
"import ipywidgets\n",
"from ipyleaflet import (\n",
" Map,\n",
" Marker,\n",
Expand Down Expand Up @@ -48,6 +49,16 @@
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"for i, f in enumerate(data[\"features\"]):\n",
" f[\"id\"] = i"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-15T18:21:31.197905Z",
Expand All @@ -65,7 +76,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 13,
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-15T18:21:36.974937Z",
Expand All @@ -76,15 +87,15 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "14649585f32d4174a701bdd9d77a8b6a",
"model_id": "5cb70b4253a84f6d956fe91b19647fae",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Map(center=[46.475212657477016, 6.3198722284199675], controls=(ZoomControl(options=['position', 'zoom_in_text'…"
]
},
"execution_count": 12,
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -104,52 +115,40 @@
"m.add(g1)\n",
"m.add(g2)\n",
"\n",
"draw_data = data[\"features\"][1]\n",
"draw_data = data[\"features\"][0]\n",
"draw_data[\"properties\"][\"type\"] = \"circlemarker\"\n",
"dc = GeomanDrawControl(\n",
" marker={},\n",
" circlemarker={},\n",
" polygon={},\n",
" data=[draw_data]\n",
" # data=[draw_data],\n",
" # hover_style={\"color\": \"red\", \"dashArray\": \"0\", \"fillOpacity\": 1, \"weight\": 8.0},\n",
" limit_markers_to_count=5,\n",
" continuous_update=False\n",
")\n",
"\n",
"def handle_draw(target, action, geo_json):\n",
" print(action)\n",
" print(geo_json)\n",
"\n",
"\n",
"def on_value_change(change):\n",
" print(\"DATA CHANGE\")\n",
" print(change)\n",
" \n",
"dc.observe(on_value_change, names='data')\n",
"dc.on_draw(handle_draw)\n",
"m.add(dc)\n",
"m"
]
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "14649585f32d4174a701bdd9d77a8b6a",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Map(bottom=46680.0, center=[46.475212657477016, 6.3198722284199675], controls=(ZoomControl(options=['position'…"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"# Modifying draw options will update the toolbar\n",
"dc.marker={\"markerStyle\": {\"color\": \"#FF0000\"}}\n",
"dc.rectangle={\"pathOptions\": {\"color\": \"#FF0000\"}}\n",
"dc.circlemarker={\"pathOptions\": {\"color\": \"#FF0000\"}}\n",
"m"
"# Explicit data sync\n",
"dc.sync_data()"
]
},
{
Expand Down
26 changes: 21 additions & 5 deletions python/ipyleaflet/ipyleaflet/leaflet.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,9 +660,9 @@ class Tooltip(UILayer):
offset: tuple, default (0, 0)
Optional offset of the tooltip position (in pixels).
direction: str, default 'auto'
Direction where to open the tooltip.
Possible values are: right, left, top, bottom, center, auto.
auto will dynamically switch between right and left according
Direction where to open the tooltip.
Possible values are: right, left, top, bottom, center, auto.
auto will dynamically switch between right and left according
to the tooltip position on the map.
permanent: bool, default False
Whether to open the tooltip permanently or only on mouseover.
Expand Down Expand Up @@ -2367,7 +2367,7 @@ class GeomanDrawControl(DrawControlBase):
circlemarker = Dict({ 'pathOptions': {} }).tag(sync=True)

# Hover style (applies for all drawing modes)
hover_style = Dict().tag(sync=True)
hover_style = Dict().tag(sync=True)

# Disabled by default
text = Dict().tag(sync=True)
Expand All @@ -2378,6 +2378,8 @@ class GeomanDrawControl(DrawControlBase):
cut = Bool(True).tag(sync=True)
rotate = Bool(True).tag(sync=True)

continuous_updates = Bool(True).tag(sync=True)

def __init__(self, **kwargs):
super(GeomanDrawControl, self).__init__(**kwargs)
self.on_msg(self._handle_leaflet_event)
Expand Down Expand Up @@ -2420,12 +2422,26 @@ def on_click(self, callback, remove=False):
Whether to remove this callback or not. Defaults to False.
"""
self._click_callbacks.register_callback(callback, remove=remove)


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

def sync_data(self):
"""Force sync the data from the Geoman side to the data traitlet.
This will cause all map data values to be sent from the client to the server.

Useful together with continuous_updates=False, to fetch the data at a specfic
point e.g. when a "save edits" button is pressed.

Note that this just triggers the client data sending, there will be a lag
between the request and the data sync being completed. Therefore to get the data
you will need to wait for the "data" traitlet to be updated, e.g. by using
traitlets.observe.
"""
self.send({"msg": "sync_data"})


class DrawControlCompatibility(DrawControlBase):
"""DrawControl class.
Expand Down
64 changes: 52 additions & 12 deletions python/jupyter_leaflet/src/controls/GeomanDrawControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ LeafletGeomanDrawControlModel.serializers = {
export class LeafletGeomanDrawControlView extends LeafletControlView {
feature_group: GeoJSON;
controlOptions: { [option: string]: any };

initialize(
parameters: WidgetView.IInitializeParameters<LeafletControlModel>
) {
Expand Down Expand Up @@ -136,8 +137,12 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {
this.data_to_layers();
this.map_view.obj.addLayer(this.feature_group);

this.setControlOptions();

this.setMode();

const continuousUpdate = this.model.get('continuous_update');

this.map_view.obj.on(
'pm:create',
(e: { shape: L.PM.SUPPORTED_SHAPES; layer: LayerShapes }) => {
Expand All @@ -157,14 +162,20 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {
geo_json: this.layer_to_json(layer),
});
this.feature_group.addLayer(layer);
this.layers_to_data();
this.model.save_changes();
if (continuousUpdate) {
this.layers_to_data();
// TODO this is called anyway in layers to data?
// this.model.save_changes();
}
});
return;
}
this.feature_group.addLayer(layer);
this.layers_to_data();
this.model.save_changes();
if (continuousUpdate) {
this.layers_to_data();
// TODO this is called anyway in layers to data?
// this.model.save_changes();
}
}
);
this.map_view.obj.on(
Expand All @@ -176,7 +187,11 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {
event: 'pm:remove',
geo_json: this.layer_to_json(eventLayer),
});
this.layers_to_data();
if (continuousUpdate) {
this.layers_to_data();
// TODO this is called anyway in layers to data?
// this.model.save_changes();
}
}
);
this.map_view.obj.on(
Expand All @@ -202,13 +217,19 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {
event: 'pm:cut',
geo_json: geo_json,
});
this.layers_to_data();
if (continuousUpdate) {
this.layers_to_data();
this.model.save_changes();
}
}
);
this.map_view.obj.on('pm:rotateend', (e: { layer: LayerShapes }) => {
var eventLayer = e.layer;
this.event_to_json('pm:rotateend', eventLayer);
this.layers_to_data();
if (continuousUpdate) {
this.layers_to_data();
this.model.save_changes();
}
});
// add listeners for syncing modes
this.map_view.obj.on(
Expand Down Expand Up @@ -454,6 +475,8 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {

data_to_layers() {
const data = this.model.get('data');
const continuousUpdate = this.model.get('continuous_update');

this.feature_group.clearLayers();
this.feature_group.addData(data);
// We add event listeners here, since these need to be added on a
Expand All @@ -462,27 +485,39 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {
layer.on('pm:vertexadded', (e: { layer: L.Layer }) => {
var eventLayer = e.layer;
this.event_to_json('pm:vertexadded', eventLayer);
this.layers_to_data();

if (continuousUpdate) {
this.layers_to_data();
}
});
layer.on('pm:vertexremoved', (e) => {
var eventLayer = e.layer;
this.event_to_json('pm:vertexremoved', eventLayer);
this.layers_to_data();
if (continuousUpdate) {
this.layers_to_data();
}
});
layer.on('pm:markerdragend', (e) => {
var eventLayer = e.layer;
this.event_to_json('pm:vertexdrag', eventLayer);
this.layers_to_data();
if (continuousUpdate) {
console.log('pm:vertexdrag data push');
this.layers_to_data();
}
});
layer.on('pm:dragend', (e) => {
var eventLayer = e.layer;
this.event_to_json('pm:drag', eventLayer);
this.layers_to_data();
if (continuousUpdate) {
this.layers_to_data();
}
});
layer.on('pm:textblur', (e) => {
var eventLayer = e.layer;
this.event_to_json('pm:textchange', eventLayer);
this.layers_to_data();
if (continuousUpdate) {
this.layers_to_data();
}
});
});
}
Expand Down Expand Up @@ -511,6 +546,11 @@ export class LeafletGeomanDrawControlView extends LeafletControlView {

handle_message(content: { msg: string }) {
switch (content.msg) {
case 'sync_data': {
this.layers_to_data();
// Return rather than break, since layers_to_data is run again further down.
return;
}
case 'clear': {
this.feature_group.eachLayer((layer) => {
this.feature_group.removeLayer(layer);
Expand Down
Loading