diff --git a/examples/GeomanDrawControl.ipynb b/examples/GeomanDrawControl.ipynb index 7815c958..641a6956 100644 --- a/examples/GeomanDrawControl.ipynb +++ b/examples/GeomanDrawControl.ipynb @@ -11,6 +11,7 @@ }, "outputs": [], "source": [ + "import ipywidgets\n", "from ipyleaflet import (\n", " Map,\n", " Marker,\n", @@ -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", @@ -65,7 +76,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": { "ExecuteTime": { "end_time": "2025-05-15T18:21:36.974937Z", @@ -76,7 +87,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "14649585f32d4174a701bdd9d77a8b6a", + "model_id": "5cb70b4253a84f6d956fe91b19647fae", "version_major": 2, "version_minor": 0 }, @@ -84,7 +95,7 @@ "Map(center=[46.475212657477016, 6.3198722284199675], controls=(ZoomControl(options=['position', 'zoom_in_text'…" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -104,20 +115,27 @@ "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" @@ -125,31 +143,12 @@ }, { "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()" ] }, { diff --git a/python/ipyleaflet/ipyleaflet/leaflet.py b/python/ipyleaflet/ipyleaflet/leaflet.py index 1c733503..558f23e1 100644 --- a/python/ipyleaflet/ipyleaflet/leaflet.py +++ b/python/ipyleaflet/ipyleaflet/leaflet.py @@ -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. @@ -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) @@ -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) @@ -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. diff --git a/python/jupyter_leaflet/src/controls/GeomanDrawControl.ts b/python/jupyter_leaflet/src/controls/GeomanDrawControl.ts index 5c39f415..e4c3f4ac 100644 --- a/python/jupyter_leaflet/src/controls/GeomanDrawControl.ts +++ b/python/jupyter_leaflet/src/controls/GeomanDrawControl.ts @@ -40,6 +40,7 @@ LeafletGeomanDrawControlModel.serializers = { export class LeafletGeomanDrawControlView extends LeafletControlView { feature_group: GeoJSON; controlOptions: { [option: string]: any }; + initialize( parameters: WidgetView.IInitializeParameters ) { @@ -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 }) => { @@ -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( @@ -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( @@ -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( @@ -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 @@ -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(); + } }); }); } @@ -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);