opengeos / leafmap

A Python package for interactive mapping and geospatial analysis with minimal coding in a Jupyter environment
https://leafmap.org
MIT License
3.2k stars 379 forks source link

.edit_vector's behavior is wired #662

Closed suredream closed 8 months ago

suredream commented 9 months ago

Environment Information

Description

What I Did

create a solara app as below $ solara run example.py

import solara
import solara as sl
from solara.components.file_drop import FileInfo
from solara import Reactive, reactive
import leafmap
import os, tempfile, sys
from io import BytesIO
from typing import Union
import random, numpy as np
from ipywidgets import widgets

import geojson, json
from shapely.geometry import shape
import shapely.wkt
import pandas as pd
import time

BUTTON_KWARGS = dict(color="primary", text=True, outlined=True)

class State:
    zoom = reactive(20)
    center = reactive((None, None))
    enroll_wkt = reactive(None)

def wkt_to_featurecollection(wkt):
    geom = shapely.wkt.loads(wkt)
    return {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "properties": {},
                "geometry": geom.__geo_interface__,
            }
        ],
    }

aoi = 'POLYGON ((-91.16138525535435 37.81442211215915, -91.16138525535435 37.73515728531591, -90.85526326612401 37.73515728531591, -90.85526326612401 37.81442211215915, -91.16138525535435 37.81442211215915))'
wkt_list = ['POLYGON ((-91.15796462083297 37.806056428087615, -91.15796462083297 37.79771581956473, -90.86679686670833 37.79771581956473, -90.86679686670833 37.806056428087615, -91.15796462083297 37.806056428087615))', 
        'POLYGON ((-91.11222224140039 37.792622288824845, -91.11222224140039 37.76260439211525, -91.02064573377882 37.76260439211525, -91.02064573377882 37.792622288824845, -91.11222224140039 37.792622288824845))',
        'POLYGON ((-91.00305251600666 37.79041596911006, -91.0496745431024 37.79041596911006, -91.0496745431024 37.74730356543847, -91.00305251600666 37.74730356543847, -91.00305251600666 37.79041596911006)))']

def widget_droplist(options, desc, width = "270px", padding = "0px 0px 0px 5px", **kwargs):
    return widgets.Dropdown(
        options=[""] + options,
        description=desc,
        style={"description_width": "initial"},
        layout=widgets.Layout(width=width, padding=padding),
        **kwargs)

def add_widgets(m, padding = "0px 0px 0px 5px"):
    style = {"description_width": "initial"}

    geom_sel = widget_droplist(['1','2','3'], "geometry:")
    export_button = widgets.Button(description="Click 'Save' before Export", layout=widgets.Layout(width="200px"))

    reset_button = widgets.Button(
        description="clear", layout=widgets.Layout(width="50px"), button_style="info"
    )

    func_box = widgets.HBox([export_button, reset_button])
    output = widgets.Output()

    # zoom to the footprint
    m.add_geojson(
        wkt_to_featurecollection(aoi),
        layer_name="Footprint",
        zoom_to_layer=True,
        # hover_style={'opacity':0.9},
        style_callback=lambda feat: {"color": "red","opacity":0.9, 'hover_style':{'opacity':0.9}},
    )

    def select_boundary(change):
        m.remove_layer(m.find_layer("Footprint"))
        m.draw_control.clear()
        m.draw_features = []
        # m.user_rois = None
        # m.user_roi = None
        # time.sleep(0.1) 

        if change.new == "1":
            feature_collection = wkt_to_featurecollection(wkt_list[0])
            m.edit_vector(feature_collection)#, layer_name="Footprint")
        elif change.new == "2":
            feature_collection = wkt_to_featurecollection(wkt_list[1])
            m.edit_vector(feature_collection)#, layer_name="Footprint2")
        elif change.new == "3":
            feature_collection = wkt_to_featurecollection(wkt_list[2])
            m.edit_vector(feature_collection)#, layer_name="Footprint2")
        else: # "empty"
            # m.draw_control.clear()
            pass
        # output.append_stdout(State.series_df.value.iloc[0]['mask'])
        output.append_stdout(change.new)
    geom_sel.observe(select_boundary, names="value")

    def export_wkt(e):
        # -1: latest saved edits
        g1 = shape(m.draw_features[-1]['geometry'])
        output.outputs = ()
        output.append_stdout(g1.wkt)

    export_button.on_click(export_wkt)

    def reset_output(e):
        output.outputs = ()
    reset_button.on_click(reset_output)

    box = widgets.VBox(
        [
            geom_sel,
            func_box,
            output,
        ]
    )
    m.add_widget(box, position="topright", add_header=False)

class Map(leafmap.Map):
    def __init__(self, **kwargs):
        kwargs["toolbar_control"] = False
        super().__init__(**kwargs)
        basemap = {
            "url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
            "attribution": "Google",
            "name": "Google Satellite",
        }
        self.add_tile_layer(**basemap, shown=True)
        add_widgets(self)

@sl.component
def Page() -> None:
    solara.Markdown("""- A widget for the user to select polygon from a list to edit, \n- however, after adding one polygon, the next one doesn't appear but the 3rd go back.\n- seems leafmap "eats" one polygon""")
    Map.element(  # type: ignore
        zoom=State.zoom.value,
        scroll_wheel_zoom=True,
        toolbar_ctrl=False,
        data_ctrl=False,
        height="780px",
    )
if __name__ == "__main__":
    Page()
suredream commented 9 months ago

image

giswqs commented 9 months ago

I am not quite sure where the bug comes from. The edit_vector() method supports loading multiple shapes. Why not just load the shapes once and let the dropdown list retrieve the list of shapes from m.draw_features?

suredream commented 9 months ago
  1. I have more than 20 shapes to load. How can I specify the shapes (Only one) I want to edit each time(by set a layer_name)?
  2. Sometimes, the loaded .edit_vector() can't be edited, it seems leafmap crashed.
suredream commented 9 months ago

What is the difference between the following variables?

suredream commented 9 months ago

I am not quite sure where the bug comes from. The edit_vector() method supports loading multiple shapes. Why not just load the shapes once and let the dropdown list retrieve the list of shapes from m.draw_features?

Not sure how to do it. Can you show me?

suredream commented 9 months ago

One more thing, my polygons usually overlay with others,that's why I don't prefer to add them together for editing(but I can display them together in different ,whichis confusing。

giswqs commented 9 months ago

ipyleaflet doesn't have advanced editing capabilities. All drawing geometries are stored in the draw control .data attribute (m.draw_control.data). You can modify the data attribute as needed.

For advanced editing functionality, you probably need to use JavaScript

https://github.com/geoman-io/leaflet-geoman

suredream commented 9 months ago

@giswqs that's beyond my plan since I still want to stay in Solara. Can we fix/bypass the multi-layer editting issue for now?

One option for me is to refresh the application frequently when the layer is "gone". But it is quite annoying.

suredream commented 9 months ago

@giswqs it's not leafmap. I can do it NOW.

import solara
import solara as sl
from solara.components.file_drop import FileInfo
from solara import Reactive, reactive
import leafmap
import os, tempfile, sys
from io import BytesIO
from typing import Union
import random, numpy as np
from ipywidgets import widgets
import copy

import geojson, json
from shapely.geometry import shape
import shapely.wkt
import pandas as pd
import time

BUTTON_KWARGS = dict(color="primary", text=True, outlined=True)

class State:
    zoom = reactive(20)
    center = reactive((None, None))
    enroll_wkt = reactive(None)

def wkt_to_featurecollection(wkt):
    geom = shapely.wkt.loads(wkt)
    return {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "properties": {},
                "geometry": geom.__geo_interface__,
            }
        ],
    }

aoi = 'POLYGON ((-91.16138525535435 37.81442211215915, -91.16138525535435 37.73515728531591, -90.85526326612401 37.73515728531591, -90.85526326612401 37.81442211215915, -91.16138525535435 37.81442211215915))'
wkt_list = ['POLYGON ((-91.15796462083297 37.806056428087615, -91.15796462083297 37.79771581956473, -90.86679686670833 37.79771581956473, -90.86679686670833 37.806056428087615, -91.15796462083297 37.806056428087615))', 
        'POLYGON ((-91.11222224140039 37.792622288824845, -91.11222224140039 37.76260439211525, -91.02064573377882 37.76260439211525, -91.02064573377882 37.792622288824845, -91.11222224140039 37.792622288824845))',
        'POLYGON ((-91.00305251600666 37.79041596911006, -91.0496745431024 37.79041596911006, -91.0496745431024 37.74730356543847, -91.00305251600666 37.74730356543847, -91.00305251600666 37.79041596911006))']

def widget_droplist(options, desc, width = "270px", padding = "0px 0px 0px 5px", **kwargs):
    return widgets.Dropdown(
        options=[""] + options,
        description=desc,
        style={"description_width": "initial"},
        layout=widgets.Layout(width=width, padding=padding),
        **kwargs)

def add_widgets(m, padding = "0px 0px 0px 5px"):
    style = {"description_width": "initial"}

    geom_sel = widget_droplist(['1','2','3'], "geometry:")
    export_button = widgets.Button(description="Click 'Save' before Export", layout=widgets.Layout(width="200px"))

    reset_button = widgets.Button(
        description="clear", layout=widgets.Layout(width="50px"), button_style="info"
    )

    func_box = widgets.HBox([export_button, reset_button])
    output = widgets.Output()

    # zoom to the footprint
    m.add_geojson(
        wkt_to_featurecollection(aoi),
        layer_name="Footprint",
        zoom_to_layer=True,
        # hover_style={'opacity':0.9},
        style_callback=lambda feat: {"color": "red","opacity":0.9, 'hover_style':{'opacity':0.9}},
    )

    def select_boundary(change):
        # https://github.com/opengeos/leafmap/blob/2c046768c180598088c572102688846289c02af8/leafmap/leafmap.py#L3829
        layer = m.find_layer("Footprint")
        if layer is None:
            print("Error: Can not find Footprint layer")
            return

        #m.draw_control.clear()
        #m.draw_features = []
        # m.user_rois = None
        # m.user_roi = None
        # time.sleep(0.1) 

        if change.new == "1":
            layer.data = wkt_to_featurecollection(wkt_list[0])
        elif change.new == "2":
            layer.data = wkt_to_featurecollection(wkt_list[1])
        elif change.new == "3":
            layer.data = wkt_to_featurecollection(wkt_list[2])
        else: # "empty"
            # m.draw_control.clear()
            pass

        draw_features = copy.deepcopy(layer.data["features"])
        for feature in draw_features:
            feature["properties"]["style"]["color"] = "green"

        m.draw_control.data = draw_features
        m.draw_features = draw_features

        # output.append_stdout(State.series_df.value.iloc[0]['mask'])
        output.append_stdout(change.new)
    geom_sel.observe(select_boundary, names="value")

    def export_wkt(e):
        # -1: latest saved edits
        g1 = shape(m.draw_features[-1]['geometry'])
        output.outputs = ()
        output.append_stdout(g1.wkt)

    export_button.on_click(export_wkt)

    def reset_output(e):
        output.outputs = ()
    reset_button.on_click(reset_output)

    box = widgets.VBox(
        [
            geom_sel,
            func_box,
            output,
        ]
    )
    m.add_widget(box, position="topright", add_header=False)

class Map(leafmap.Map):
    def __init__(self, **kwargs):
        kwargs["toolbar_control"] = False
        super().__init__(**kwargs)
        basemap = {
            "url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
            "attribution": "Google",
            "name": "Google Satellite",
        }
        self.add_tile_layer(**basemap, shown=True)
        add_widgets(self)

@sl.component
def Page() -> None:
    solara.Markdown("""- A widget for the user to select polygon from a list to edit, \n- however, after adding one polygon, the next one doesn't appear but the 3rd go back.\n- seems leafmap "eats" one polygon""")
    Map.element(  # type: ignore
        zoom=State.zoom.value,
        scroll_wheel_zoom=True,
        toolbar_ctrl=False,
        data_ctrl=False,
        height="780px",
    )
if __name__ == "__main__":
    Page()