Support on_click popup for GeoJSON layer
giswqs opened this issue · 9 comments
giswqs commented
Reference issue: jupyter-widgets/ipyleaflet#371
import ipywidgets as widgets
from ipyleaflet import Map, Polygon
polygon = Polygon(
locations=[(42, -49), (43, -49), (43, -48)],
color="green",
fill_color="green"
)
m = Map(center=(42.5531, -48.6914), zoom=6)
m.add(polygon)
polygon.popup = widgets.HTML('Hello World!!')
m
giswqs commented
A complete example. One limitation is that the event handler can't capture the mouse clicked location. The polygon centroid is used as a workaround.
from ipyleaflet import Map, GeoJSON, Popup
import json
from shapely.geometry import shape
import ipywidgets as widgets
# Create a map centered at a specific location
m = Map(center=(51.55, -0.09), zoom=10)
# Example GeoJSON data with two polygons
geojson_data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Polygon A",
"popup_content": "This is Polygon A."
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-0.1, 51.5],
[-0.1, 51.6],
[-0.05, 51.6],
[-0.05, 51.5],
[-0.1, 51.5]
]]
}
},
{
"type": "Feature",
"properties": {
"name": "Polygon B",
"popup_content": "This is Polygon B."
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-0.08, 51.48],
[-0.08, 51.52],
[-0.03, 51.52],
[-0.03, 51.48],
[-0.08, 51.48]
]]
}
}
]
}
# Create a GeoJSON layer
geojson_layer = GeoJSON(data=geojson_data)
output = widgets.Output()
# Function to calculate the centroid of a polygon
def calculate_centroid(polygon_coordinates):
polygon = shape({
"type": "Polygon",
"coordinates": polygon_coordinates
})
centroid = polygon.centroid
return centroid.y, centroid.x # Return as (lat, lon)
# Define a function to handle the on-click event
def on_click_handler(event, feature, **kwargs):
# Calculate the centroid of the polygon
centroid = calculate_centroid(feature['geometry']['coordinates'])
with output:
print(event)
print(centroid)
print(feature['properties']['popup_content'])
# Create a popup with the content from GeoJSON properties
popup = Popup(
location=centroid,
child=widgets.HTML(feature['properties']['popup_content']),
close_button=False,
auto_close=True
)
# Add the popup to the map
m.add_layer(popup)
# Bind the click event to the GeoJSON layer
geojson_layer.on_click(on_click_handler)
# Add the GeoJSON layer to the map
m.add_layer(geojson_layer)
# Display the map
m
giswqs commented
Marker on_click event
from ipyleaflet import Map, CircleMarker
m = Map(center=(52.204793, 0.121558), zoom=15)
marker = CircleMarker(location=(52.204793, 0.121558), radius=10, color="red", fill_opacity=0.7)
def on_click_handler(**kwargs):
print(f"CircleMarker clicked at location: {marker.location}")
marker.on_click(on_click_handler)
m.add_layer(marker)
m
giswqs commented
Example for editing data.
import requests
import ipywidgets as widgets
from ipyleaflet import Map, CircleMarker, Popup
# URL to the GeoJSON data
geojson_url = "https://github.com/opengeos/datasets/releases/download/world/world_cities.geojson"
# Download the GeoJSON data
response = requests.get(geojson_url)
geojson_data = response.json()
# Create a map centered on a point in the US
m = Map(center=(39.8283, -98.5795), zoom=4, scroll_wheel_zoom=True)
m.layout.height = '600px'
def create_popup_widget(circle_marker, properties):
"""Create a popup widget to change circle properties and display point attributes."""
radius_slider = widgets.IntSlider(
value=circle_marker.radius,
min=1,
max=50,
description='Radius:',
continuous_update=False
)
color_picker = widgets.ColorPicker(
value=circle_marker.color,
description='Color:',
continuous_update=False
)
fill_color_picker = widgets.ColorPicker(
value=circle_marker.fill_color,
description='Fill color:',
continuous_update=False
)
name_text = widgets.Text(
value=properties.get('name', ''),
description='Name:',
continuous_update=False
)
def update_circle(change):
"""Update circle properties based on widget values."""
circle_marker.radius = radius_slider.value
circle_marker.color = color_picker.value
circle_marker.fill_color = fill_color_picker.value
properties['name'] = name_text.value
# Link widgets to update the circle marker properties and point attributes
radius_slider.observe(update_circle, 'value')
color_picker.observe(update_circle, 'value')
fill_color_picker.observe(update_circle, 'value')
name_text.observe(update_circle, 'value')
# Arrange widgets in a vertical box with increased width
vbox = widgets.VBox(
[name_text, radius_slider, color_picker, fill_color_picker],
layout=widgets.Layout(width='310px') # Set the width of the popup widget
)
return vbox
def create_on_click_handler(circle_marker, properties):
"""Create an on_click handler with the circle_marker bound."""
def on_click(**kwargs):
if kwargs.get('type') == 'click':
# Create a popup widget with controls
popup_widget = create_popup_widget(circle_marker, properties)
popup = Popup(
location=circle_marker.location,
child=popup_widget,
close_button=True,
auto_close=False,
close_on_escape_key=True,
min_width=300,
)
m.add_layer(popup)
popup.open = True
return on_click
# Iterate over each feature in the GeoJSON data and create a CircleMarker
for feature in geojson_data['features']:
coordinates = feature['geometry']['coordinates']
properties = feature['properties']
circle_marker = CircleMarker(
location=(coordinates[1], coordinates[0]), # (lat, lon)
radius=5, # Initial radius of the circle
color="blue", # Outline color
fill_color="blue", # Fill color
fill_opacity=0.6,
)
# Create and bind the on_click handler for each circle_marker
circle_marker.on_click(create_on_click_handler(circle_marker, properties))
# Add the circle marker to the map
m.add_layer(circle_marker)
# Display the map
m
giswqs commented
Edit points interactively
import requests
import ipywidgets as widgets
from ipyleaflet import Map, CircleMarker, Popup
import leafmap
import ipyleaflet
# URL to the GeoJSON data
geojson_url = "https://github.com/opengeos/datasets/releases/download/us/cities.geojson"
# Download the GeoJSON data
response = requests.get(geojson_url)
geojson_data = response.json()
# Create a map centered on a point in the US
m = leafmap.Map(center=(39.8283, -98.5795), zoom=4, scroll_wheel_zoom=True)
m.layout.height = '600px'
widget_width = "250px"
def create_popup_widget(circle_marker, properties, original_properties, display_properties=None):
"""Create a popup widget to change circle properties and edit feature attributes."""
# Widgets for circle properties
radius_slider = widgets.IntSlider(
value=circle_marker.radius,
min=1,
max=50,
description='Radius:',
continuous_update=False,
layout=widgets.Layout(width=widget_width)
)
color_picker = widgets.ColorPicker(
value=circle_marker.color,
description='Color:',
continuous_update=False,
layout=widgets.Layout(width=widget_width)
)
fill_color_picker = widgets.ColorPicker(
value=circle_marker.fill_color,
description='Fill color:',
continuous_update=False,
layout=widgets.Layout(width=widget_width)
)
# Widgets for feature properties
property_widgets = {}
display_properties = display_properties or properties.keys()
for key in display_properties:
value = properties.get(key, "")
if isinstance(value, str):
widget = widgets.Text(
value=value,
description=f'{key}:',
continuous_update=False,
layout=widgets.Layout(width=widget_width)
)
elif isinstance(value, (int, float)):
widget = widgets.FloatText(
value=value,
description=f'{key}:',
continuous_update=False,
layout=widgets.Layout(width=widget_width)
)
else:
widget = widgets.Label(value=f'{key}: {value}', layout=widgets.Layout(width=widget_width))
property_widgets[key] = widget
def update_circle(change):
"""Update circle properties based on widget values."""
circle_marker.radius = radius_slider.value
circle_marker.color = color_picker.value
circle_marker.fill_color = fill_color_picker.value
for key, widget in property_widgets.items():
properties[key] = widget.value
def reset_circle(change):
"""Reset circle properties to their original values."""
circle_marker.radius = original_properties['radius']
circle_marker.color = original_properties['color']
circle_marker.fill_color = original_properties['fill_color']
radius_slider.value = original_properties['radius']
color_picker.value = original_properties['color']
fill_color_picker.value = original_properties['fill_color']
for key, widget in property_widgets.items():
widget.value = original_properties['properties'].get(key, "")
# Link widgets to update the circle marker properties and point attributes
radius_slider.observe(update_circle, 'value')
color_picker.observe(update_circle, 'value')
fill_color_picker.observe(update_circle, 'value')
for widget in property_widgets.values():
widget.observe(update_circle, 'value')
# Reset button
reset_button = widgets.Button(
description='Reset',
layout=widgets.Layout(width=widget_width)
)
reset_button.on_click(reset_circle)
# Arrange widgets in a vertical box with increased width
vbox = widgets.VBox(
[radius_slider, color_picker, fill_color_picker] + list(property_widgets.values()) + [reset_button],
layout=widgets.Layout(width='310px') # Set the width of the popup widget
)
return vbox
def create_on_click_handler(circle_marker, properties, display_properties=None):
"""Create an on_click handler with the circle_marker bound."""
# Save the original properties for reset
original_properties = {
'radius': circle_marker.radius,
'color': circle_marker.color,
'fill_color': circle_marker.fill_color,
'properties': properties.copy()
}
def on_click(**kwargs):
if kwargs.get('type') == 'click':
# Create a popup widget with controls
popup_widget = create_popup_widget(circle_marker, properties, original_properties, display_properties)
popup = Popup(
location=circle_marker.location,
child=popup_widget,
close_button=True,
auto_close=False,
close_on_escape_key=True,
min_width=int(widget_width[:-2]) + 10,
)
m.add_layer(popup)
popup.open = True
return on_click
layers = []
# Specify which properties to display in the popup
display_properties = ['name', 'population'] # Example: Show only 'name' and 'population'
# display_properties = None # Example: Show only 'name' and 'population'
# Iterate over each feature in the GeoJSON data and create a CircleMarker
for feature in geojson_data['features']:
coordinates = feature['geometry']['coordinates']
properties = feature['properties']
circle_marker = CircleMarker(
location=(coordinates[1], coordinates[0]), # (lat, lon)
radius=5, # Initial radius of the circle
color="white", # Outline color
weight=1, # Outline
fill_color="#3388ff", # Fill color
fill_opacity=0.6,
)
# Create and bind the on_click handler for each circle_marker
circle_marker.on_click(create_on_click_handler(circle_marker, properties, display_properties))
# Add the circle marker to the map
layers.append(circle_marker)
group = ipyleaflet.LayerGroup(layers=tuple(layers), name="Markers")
m.add(group)
# Display the map
m
giswqs commented
Example for editing polygons
import json
from ipyleaflet import Map, GeoJSON, Popup
from ipywidgets import VBox, Button, Layout, Text
import requests
from shapely.geometry import shape
import copy
# URL to the GeoJSON data
geojson_url = "https://github.com/opengeos/datasets/releases/download/us/us_states.geojson"
# Download the GeoJSON data
response = requests.get(geojson_url)
geojson_data = response.json()
# Create map
m = Map(scroll_wheel_zoom=True)
widget_width = "250px"
layout = Layout(width=widget_width)
def calculate_centroid(polygon_coordinates, geom_type):
polygon = shape({
"type": geom_type,
"coordinates": polygon_coordinates
})
centroid = polygon.centroid
return centroid.y, centroid.x # Return as (lat, lon)
def create_property_widgets(properties):
"""Dynamically create widgets for each property."""
widgets_list = []
for key, value in properties.items():
if isinstance(value, dict):
continue
widget = Text(value=str(value), description=f'{key}:', layout=layout)
widget._property_key = key # Store the key in the widget for easy access later
widgets_list.append(widget)
return widgets_list
def on_click(event, feature, **kwargs):
# Dynamically create input widgets for each property
property_widgets = create_property_widgets(feature['properties'])
save_button = Button(description="Save", layout=layout)
geom_type = feature['geometry']['type']
centroid = calculate_centroid(feature['geometry']['coordinates'], geom_type)
# Create and open the popup
popup_content = VBox(property_widgets + [save_button])
popup = Popup(
location=centroid,
child=popup_content,
close_button=True,
auto_close=False,
close_on_escape_key=True,
min_width=int(widget_width[:-2]) + 5,
)
m.add_layer(popup)
def save_changes(_):
original_data = copy.deepcopy(geojson_layer.data)
original_feature = copy.deepcopy(feature)
# Update the properties with the new values
for widget in property_widgets:
feature['properties'][widget._property_key] = widget.value
for i, f in enumerate(original_data['features']):
if f == original_feature:
original_data['features'][i] = feature
break
# Update the GeoJSON layer to reflect the changes
geojson_layer.data = original_data
m._geojson_data = original_data
m.remove_layer(popup) # Close the popup by removing it from the map
save_button.on_click(save_changes)
# Add GeoJSON layer to the map
geojson_layer = GeoJSON(data=geojson_data, style={'color': '#3388ff'}, name='GeoJSON Layer')
# Attach click event to the GeoJSON layer
geojson_layer.on_click(on_click)
# Add layers to map
m.add_layer(geojson_layer)
m._geojson_data = geojson_layer.data
m.center = [40, -100]
m.zoom = 4
m
giswqs commented
Edit feature properties and save changes
from ipyleaflet import Map, GeoJSON, Popup
import ipywidgets as widgets
import requests
from shapely.geometry import shape
import copy
import geopandas as gpd
# URL to the GeoJSON data
geojson_url = "https://github.com/opengeos/datasets/releases/download/places/nyc_buildings.geojson"
# geojson_url = "https://github.com/opengeos/datasets/releases/download/places/nyc_roads.geojson"
# Download the GeoJSON data
response = requests.get(geojson_url)
geojson_data = response.json()
# Create map
m = Map(scroll_wheel_zoom=True)
m.layout.height = '600px'
widget_width = "250px"
layout = widgets.Layout(width=widget_width)
def calculate_centroid(polygon_coordinates, geom_type):
polygon = shape({
"type": geom_type,
"coordinates": polygon_coordinates
})
centroid = polygon.centroid
return centroid.y, centroid.x # Return as (lat, lon)
def create_property_widgets(properties):
"""Dynamically create widgets for each property."""
widgets_list = []
for key, value in properties.items():
if key == "style":
continue
if isinstance(value, (int, float)):
widget = widgets.FloatText(value=value, description=f'{key}:', layout=layout)
else:
widget = widgets.Text(value=str(value), description=f'{key}:', layout=layout)
widget._property_key = key # Store the key in the widget for easy access later
widgets_list.append(widget)
return widgets_list
def on_click(event, feature, **kwargs):
# Dynamically create input widgets for each property
property_widgets = create_property_widgets(feature['properties'])
save_button = widgets.Button(description="Save", layout=layout)
geom_type = feature['geometry']['type']
centroid = calculate_centroid(feature['geometry']['coordinates'], geom_type)
# Create and open the popup
popup_content = widgets.VBox(property_widgets + [save_button])
popup = Popup(
location=centroid,
child=popup_content,
close_button=True,
auto_close=False,
close_on_escape_key=True,
min_width=int(widget_width[:-2]) + 5,
)
m.add_layer(popup)
def save_changes(_):
original_data = copy.deepcopy(geojson_layer.data)
original_feature = copy.deepcopy(feature)
# Update the properties with the new values
for widget in property_widgets:
feature['properties'][widget._property_key] = widget.value
for i, f in enumerate(original_data['features']):
if f == original_feature:
original_data['features'][i] = feature
# print(original_feature)
break
# Update the GeoJSON layer to reflect the changes
geojson_layer.data = original_data
m._geojson_data = original_data
m.remove_layer(popup) # Close the popup by removing it from the map
save_button.on_click(save_changes)
# Add GeoJSON layer to the map
geojson_layer = GeoJSON(data=geojson_data, style={'color': '#3388ff'}, hover_style={"color": "yellow", "weight": 5}, name='GeoJSON Layer')
# Attach click event to the GeoJSON layer
geojson_layer.on_click(on_click)
# Add layers to map
m.add_layer(geojson_layer)
m._geojson_data = geojson_layer.data
m.center = [40.7131, -73.9923]
m.zoom = 14
m
giswqs commented
Edit points, lines, and polygons interactively.