opengeos/leafmap

Add support for selecting/editing multiple features

giswqs opened this issue · 3 comments

Selecting and highlight multiple features

import os
import json
import requests
import copy
from ipyleaflet import Map, GeoJSON, LayersControl

def create_geojson_map(data, style, hover_style, highlight_style, center=(50.6252978589571, 0.34580993652344), zoom=4):
    """
    Create a GeoJSON map with click highlighting functionality.
    
    Parameters:
    - data: GeoJSON data
    - style: Default style for the GeoJSON layer
    - hover_style: Style when hovering over features
    - highlight_style: Style to apply when a feature is clicked
    - center: Initial center of the map (default: Europe)
    - zoom: Initial zoom level (default: 4)
    """
    
    # Create the map
    m = Map(center=center, zoom=zoom, scroll_wheel_zoom=True)

    # Function to highlight the clicked feature
    def highlight_feature(event, feature, **kwargs):
        original_data = copy.deepcopy(geo_json.data)

        for index, f in enumerate(original_data['features']):
            if f['properties']['name'] == feature['properties']['name']:  
                if "fillColor" in original_data['features'][index]['properties']['style']:
                    color = original_data['features'][index]['properties']['style']['fillColor']
                    if color == "yellow":
                        original_data['features'][index]['properties']['style'] = style
                    else:       
                        original_data['features'][index]['properties']['style'] = highlight_style
                else:
                    original_data['features'][index]['properties']['style'] = highlight_style
                break

        geo_json.data = original_data

    # Create the GeoJSON layer
    geo_json = GeoJSON(
        data=data,
        style=style,
        hover_style=hover_style,
        name='Countries'
    )

    # Add click event to highlight features
    geo_json.on_click(highlight_feature)

    # Add the GeoJSON layer to the map
    m.add_layer(geo_json)

    # Add a layer control
    control = LayersControl(position='topright')
    m.add_control(control)

    return m


# Download the GeoJSON file if it doesn't exist
if not os.path.exists('europe_110.geo.json'):
    url = 'https://raw.githubusercontent.com/jupyter-widgets/ipyleaflet/master/examples/europe_110.geo.json'
    r = requests.get(url)
    with open('europe_110.geo.json', 'w') as f:
        f.write(r.content.decode("utf-8"))

# Load the GeoJSON data
with open('europe_110.geo.json', 'r') as f:
    data = json.load(f)

# Default styles
style = {"color": "#3388ff"}
hover_style = {'color': 'yellow', 'dashArray': '0', 'fillOpacity': 0.3}
highlight_style = {'color': '#3388ff', 'fillColor': 'yellow', 'weight': 3, 'fillOpacity': 0.5}

# Create and display the map
m = create_geojson_map(data, style, hover_style, highlight_style)
m

Peek 2024-09-05 23-22

import os
import json
import requests
import copy
import ipywidgets as widgets
from ipyleaflet import Map, GeoJSON, LayersControl, WidgetControl
from ipywidgets import VBox

def create_geojson_map(data, style, hover_style, highlight_style, display_props=None, center=(50.6252978589571, 0.34580993652344), zoom=4):
    """
    Create a GeoJSON map with hover and click functionality, dynamic attribute editors, and a clear selection button.
    The attribute editor will be displayed inside the map using ipyleaflet's WidgetControl.
    
    Parameters:
    - data: GeoJSON data
    - style: Default style for the GeoJSON layer
    - hover_style: Style when hovering over features
    - highlight_style: Style to apply when a feature is clicked
    - display_props: List of attribute keys to display in the widgets (default: show all attributes)
    - center: Initial center of the map (default: Europe)
    - zoom: Initial zoom level (default: 4)
    """
    
    # Create the map
    m = Map(center=center, zoom=zoom, scroll_wheel_zoom=True)
    
    # List to store the IDs of highlighted features
    highlighted_features = []
    
    # Create a dictionary to hold attribute widgets
    attribute_widgets = {}

    # Get the keys from the first feature to dynamically create widgets
    first_feature = data['features'][0]['properties']
    
    # If display_props is not provided, show all attributes
    if display_props is None:
        display_props = first_feature.keys()

    # Loop through only the specified properties in display_props
    for key in display_props:
        if key in first_feature:  # Ensure the property exists
            attribute_widgets[key] = widgets.Text(description=f"{key.capitalize()}:")
             
            # value = first_feature[key]
            # if isinstance(value, str):
            #     attribute_widgets[key] = widgets.Text(description=f"{key.capitalize()}:")
            # elif isinstance(value, int):
            #     attribute_widgets[key] = widgets.IntText(description=f"{key.capitalize()}:")
            # elif isinstance(value, float):
            #     attribute_widgets[key] = widgets.FloatText(description=f"{key.capitalize()}:")
            # Add more types as needed (e.g., bool, list, etc.)

    # Update button and clear selection button
    update_button = widgets.Button(description="Update Attributes")
    clear_button = widgets.Button(description="Clear Selection")
    
    # Function to highlight the clicked feature and clear attribute fields
    def highlight_feature(event, feature, **kwargs):
        nonlocal highlighted_features
        original_data = copy.deepcopy(geo_json.data)

        for index, f in enumerate(original_data['features']):
            if f['properties']['name'] == feature['properties']['name']:
                if index in highlighted_features:
                    highlighted_features.remove(index)
                    original_data['features'][index]['properties']['style'] = style
                else:
                    highlighted_features.append(index)
                    original_data['features'][index]['properties']['style'] = highlight_style

                # Clear the widget fields when a feature is clicked
                for key, widget in attribute_widgets.items():
                        widget.value = ''
                        widget.placeholder = ""
                    # if type(widget) == widgets.Text:
                    #     widget.value = ''
                    #     widget.placeholder = "ABD"
                    # elif type(widget) == widgets.IntText:
                    #     widget.value = 0
                    # elif type(widget) == widgets.FloatText:
                    #     widget.value = 0

        geo_json.data = original_data

    # Function to clear the selection
    def clear_selection(_):
        original_data = copy.deepcopy(geo_json.data)

        # Reset the style for all highlighted features
        for index in highlighted_features:
            original_data['features'][index]['properties']['style'] = style
        
        highlighted_features.clear()
        geo_json.data = original_data

    # Function to apply changes to highlighted features
    def update_highlighted_features(_):
        original_data = copy.deepcopy(geo_json.data)

        # Update the properties for all highlighted features
        for index in highlighted_features:
            for key, widget in attribute_widgets.items():
                original_data['features'][index]['properties'][key] = widget.value

        geo_json.data = original_data
        clear_selection(None)
        for key, widget in attribute_widgets.items():
            widget.value = ""

    # Function to populate attribute fields on hover
    def populate_hover_attributes(event, feature, **kwargs):
        # Populate the widget fields with the hovered feature's attributes
        for key, widget in attribute_widgets.items():
            # widget.value = feature['properties'].get(key, '')
            if widget.value.strip() == "":
                widget.value = ""
                widget.placeholder = str(feature['properties'].get(key, ''))

    # Create the GeoJSON layer
    geo_json = GeoJSON(
        data=data,
        style=style,
        hover_style=hover_style,
        name='Countries'
    )

    # Add click event to highlight features and clear attribute fields
    geo_json.on_click(highlight_feature)

    # Add hover event to populate attribute fields
    geo_json.on_hover(populate_hover_attributes)

    # Add the GeoJSON layer to the map
    m.add_layer(geo_json)

    # Add a layer control
    control = LayersControl(position='topright')
    m.add_control(control)

    # Add event listeners to the buttons
    update_button.on_click(update_highlighted_features)
    clear_button.on_click(clear_selection)

    # Create a VBox to hold the widgets for editing attributes and the buttons
    attribute_editor = VBox([*attribute_widgets.values(), update_button, clear_button])

    # Embed the attribute editor inside the map using WidgetControl
    widget_control = WidgetControl(widget=attribute_editor, position='topright')
    m.add_control(widget_control)

    return m


# Download the GeoJSON file if it doesn't exist
if not os.path.exists('europe_110.geo.json'):
    url = 'https://raw.githubusercontent.com/jupyter-widgets/ipyleaflet/master/examples/europe_110.geo.json'
    r = requests.get(url)
    with open('europe_110.geo.json', 'w') as f:
        f.write(r.content.decode("utf-8"))

# Load the GeoJSON data
with open('europe_110.geo.json', 'r') as f:
    data = json.load(f)

# Default styles
style = {"color": "#3388ff"}
hover_style = {'color': 'yellow', 'dashArray': '0', 'fillOpacity': 0.3}
highlight_style = {'color': '#3388ff', 'fillColor': 'yellow', 'weight': 3, 'fillOpacity': 0.5}

# Example usage: Specify certain properties to display
display_props = ['name', 'pop_est']

# Create and display the map with the widget control
m = create_geojson_map(data, style, hover_style, highlight_style, display_props=display_props)
m
import os
import json
import requests
import copy
import ipywidgets as widgets
from ipyleaflet import Map, GeoJSON, LayersControl, WidgetControl
# from ipywidgets import VBox, HBox
import ipywidgets as widgets

def create_geojson_map(data, style, hover_style, highlight_style, changed_style, display_props=None, center=[40.71278477995827, -73.99725437164308], zoom=14):
    """
    Create a GeoJSON map with hover and click functionality, dynamic attribute editors, and a clear selection button.
    The attribute editor will be displayed inside the map using ipyleaflet's WidgetControl.
    
    Parameters:
    - data: GeoJSON data
    - style: Default style for the GeoJSON layer
    - hover_style: Style when hovering over features
    - highlight_style: Style to apply when a feature is clicked
    - display_props: List of attribute keys to display in the widgets (default: show all attributes)
    - center: Initial center of the map (default: Europe)
    - zoom: Initial zoom level (default: 4)
    """
    
    # Create the map
    m = Map(center=center, zoom=zoom, scroll_wheel_zoom=True)
    
    # List to store the IDs of highlighted features
    highlighted_features = []
    
    # Create a dictionary to hold attribute widgets
    attribute_widgets = {}

    # Get the keys from the first feature to dynamically create widgets
    first_feature = data['features'][0]['properties']
    
    # If display_props is not provided, show all attributes
    if display_props is None:
        display_props = first_feature.keys()

    text_width = "250px"
    text_layout = widgets.Layout(width=text_width)
    # Loop through only the specified properties in display_props
    for key in display_props:
        if key in first_feature:  # Ensure the property exists
            attribute_widgets[key] = widgets.Text(description=f"{key.capitalize()}:", layout=text_layout)
             
            # value = first_feature[key]
            # if isinstance(value, str):
            #     attribute_widgets[key] = widgets.Text(description=f"{key.capitalize()}:")
            # elif isinstance(value, int):
            #     attribute_widgets[key] = widgets.IntText(description=f"{key.capitalize()}:")
            # elif isinstance(value, float):
            #     attribute_widgets[key] = widgets.FloatText(description=f"{key.capitalize()}:")
            # Add more types as needed (e.g., bool, list, etc.)

    # Update button and clear selection button
    button_width = "80px"
    button_layout = widgets.Layout(width=button_width)
    update_button = widgets.Button(description="Update", layout=button_layout)
    clear_button = widgets.Button(description="Clear", layout=button_layout)
    close_button = widgets.Button(description="Close", layout=button_layout)
    
    # Function to highlight the clicked feature and clear attribute fields
    def highlight_feature(event, feature, **kwargs):
        nonlocal highlighted_features
        original_data = copy.deepcopy(geo_json.data)

        for index, f in enumerate(original_data['features']):
            if f == feature:
            # if f['properties']['name'] == feature['properties']['name']:
                if index in highlighted_features:
                    highlighted_features.remove(index)
                    original_data['features'][index]['properties']['style'] = style
                else:
                    highlighted_features.append(index)
                    original_data['features'][index]['properties']['style'] = highlight_style

                # Clear the widget fields when a feature is clicked
                # for key, widget in attribute_widgets.items():
                #         widget.value = ''
                #         widget.placeholder = ""
                    # if type(widget) == widgets.Text:
                    #     widget.value = ''
                    #     widget.placeholder = "ABD"
                    # elif type(widget) == widgets.IntText:
                    #     widget.value = 0
                    # elif type(widget) == widgets.FloatText:
                    #     widget.value = 0

        geo_json.data = original_data

    # Function to clear the selection
    def clear_selection(_):
        original_data = copy.deepcopy(geo_json.data)

        # Reset the style for all highlighted features
        for index in highlighted_features:
            if original_data['features'][index]['properties']['style'] != changed_style:
                original_data['features'][index]['properties']['style'] = style
        
        highlighted_features.clear()
        geo_json.data = original_data       

    # Function to apply changes to highlighted features
    def update_highlighted_features(_):
        original_data = copy.deepcopy(geo_json.data)

        # Update the properties for all highlighted features
        for index in highlighted_features:
            for key, widget in attribute_widgets.items():
                if widget.value.strip() != "":
                    dtype = type(original_data['features'][index]['properties'][key])
                    # original_data['features'][index]['properties'][key] = widget.value
                    if dtype == str:
                        value = str(widget.value)
                    elif dtype == int:
                        value = int(widget.value)
                    elif dtype == float:
                        value = float(widget.value)
                    else:
                        value = widget.value
                    original_data['features'][index]['properties'][key] = value
                    original_data['features'][index]['properties']['style'] = changed_style


        geo_json.data = original_data
        clear_selection(None)
        for key, widget in attribute_widgets.items():
            widget.value = ""

    # Function to populate attribute fields on hover
    def populate_hover_attributes(event, feature, **kwargs):
        # Populate the widget fields with the hovered feature's attributes
        for key, widget in attribute_widgets.items():
            # widget.value = feature['properties'].get(key, '')
            if widget.value.strip() == "":
                widget.value = ""
                widget.placeholder = str(feature['properties'].get(key, ''))

    # Create the GeoJSON layer
    geo_json = GeoJSON(
        data=data,
        style=style,
        hover_style=hover_style,
        name='Countries'
    )

    # Add click event to highlight features and clear attribute fields
    geo_json.on_click(highlight_feature)

    # Add hover event to populate attribute fields
    geo_json.on_hover(populate_hover_attributes)

    # Add the GeoJSON layer to the map
    m.add_layer(geo_json)

    # Add a layer control
    control = LayersControl(position='topright')
    m.add_control(control)

    # Add event listeners to the buttons
    update_button.on_click(update_highlighted_features)
    clear_button.on_click(clear_selection)

    # Create a VBox to hold the widgets for editing attributes and the buttons
    buttons = widgets.HBox([update_button, clear_button, close_button])
    attribute_editor = widgets.VBox([*attribute_widgets.values(), buttons])

    # Embed the attribute editor inside the map using WidgetControl
    widget_control = WidgetControl(widget=attribute_editor, position='topright')
    m.add_control(widget_control)

    def close_widget_control(_):
        m.remove(widget_control)

    close_button.on_click(close_widget_control)

    return m


# Download the GeoJSON file if it doesn't exist
if not os.path.exists('europe_110.geo.json'):
    url = 'https://github.com/opengeos/datasets/releases/download/places/nyc_buildings.geojson'
    r = requests.get(url)
    with open('nyc_buildings.geojson', 'w') as f:
        f.write(r.content.decode("utf-8"))

# Load the GeoJSON data
with open('nyc_buildings.geojson', 'r') as f:
    data = json.load(f)

# Default styles
style = {"color": "#3388ff"}
hover_style = {'color': 'yellow', 'dashArray': '0', 'fillOpacity': 0.3}
highlight_style = {'color': '#3388ff', 'fillColor': 'yellow', 'weight': 3, 'fillOpacity': 0.5}
changed_style = {'color': '#3388ff', 'fillColor': 'red', 'weight': 3, 'fillOpacity': 0.3}

# Example usage: Specify certain properties to display
display_props = ['name', 'pop_est']

# Create and display the map with the widget control
m = create_geojson_map(data, style, hover_style, highlight_style, changed_style, display_props=None)
m.layout.height="600px"
m

image

Implemented in #886