Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull Request Overview
This PR adds a comprehensive vector editing feature to MapLibreMap by introducing the add_vector_editor() method, which combines Geoman drawing controls with an interactive property editor widget.
Key Changes:
- Adds
add_vector_editor()method that loads vector data (GeoJSON, shapefiles, URLs, or GeoDataFrames) and provides an interactive UI for editing feature geometries and properties - Implements automatic property inference from data when properties aren't explicitly specified
- Adds
geojson_bounds()utility function to calculate spatial bounds using shapely - Modifies JavaScript to use delete/import approach for feature loading to ensure proper editability
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| anymap/maplibre.py | Adds the main add_vector_editor method (470 lines) with support for loading various vector formats, property widgets, and feature management |
| anymap/utils.py | Adds geojson_bounds helper function to calculate bounding boxes for GeoJSON data |
| anymap/static/maplibre_widget.js | Simplifies Geoman feature import logic to always use delete/import approach for proper editability |
| docs/examples/maplibre/add_vector_editor_example.ipynb | Comprehensive Jupyter notebook with 5 examples demonstrating various use cases and features |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return os.path.abspath(output) | ||
|
|
||
|
|
||
| def geojson_bounds(geojson: dict) -> Optional[list]: |
There was a problem hiding this comment.
Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.
|
🚀 Deployed on https://691e0c2e8083023be1a91f9f--opengeos.netlify.app |
Member
Author
output = widgets.Output()
def handle_map_interaction(**kwargs):
with output:
if kwargs.get("type") == "click":
print(kwargs)
# elif kwargs.get("type") == "mousemove":
# print(kwargs)
m.on_interaction(handle_map_interaction)
output |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Member
Author
output = widgets.Output()
def handle_map_interaction(**kwargs):
status = m.get_geoman_status()
active = [s.lower() for s in status.get('activeButtons', [])]
if "snapping" in active:
active.remove("snapping")
with output:
if kwargs.get("type") == "click" and not active:
print(kwargs)
m.on_interaction(handle_map_interaction)
output |
Member
Author
import copy
import anymap
import duckdb
import ipywidgets as widgets
from anymap.utils import geojson_intersect_duckdb, geojson_to_duckdb
con = duckdb.connect()
con.install_extension("httpfs")
con.install_extension("spatial")
con.load_extension("httpfs")
con.load_extension("spatial")
filepath = "buildings.parquet"
con.sql(
f"CREATE TABLE IF NOT EXISTS buildings AS SELECT * FROM read_parquet('{filepath}');"
)
m = anymap.MapLibreMap(center=[-117.592133766, 47.653004], zoom=15.3)
m.add_basemap("Esri.WorldImagery", visible=True, before_id=m.first_symbol_layer_id)
m.add_vector(
filepath,
opacity=0.5,
paint={"fill-color": "rgba(0,0,0,0)", "fill-outline-color": "rgba(255,0,0,255)"},
)
m.add_geoman_control()
m
output = widgets.Output()
def handle_map_interaction(**kwargs):
status = m.get_geoman_status()
active = [s.lower() for s in status.get("activeButtons", [])]
if "snapping" in active:
active.remove("snapping")
with output:
if kwargs.get("type") == "click" and not active:
point = {"type": "Point", "coordinates": kwargs["coordinates"]}
intersect_flag = False
if len(m.get_geoman_data()["features"]) > 0:
geojson_to_duckdb(m.get_geoman_data(), "draw_features", con)
if not geojson_intersect_duckdb(point, "draw_features", con).empty:
intersect_flag = True
if not intersect_flag:
gdf = geojson_intersect_duckdb(point, "buildings", con)
if len(gdf) > 0:
new_feature = gdf.__geo_interface__["features"][0]
if "bbox" in new_feature:
del new_feature["bbox"]
if "id" in new_feature:
del new_feature["id"]
existing_features = copy.deepcopy(m.get_geoman_data())
existing_features["features"].append(new_feature)
m.set_geoman_data(existing_features)
m.on_interaction(handle_map_interaction)
output |
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

No description provided.