Skip to content

Add vector_editor method#122

Merged
giswqs merged 17 commits intomainfrom
vector-editor
Nov 19, 2025
Merged

Add vector_editor method#122
giswqs merged 17 commits intomainfrom
vector-editor

Conversation

@giswqs
Copy link
Copy Markdown
Member

@giswqs giswqs commented Nov 17, 2025

No description provided.

Copilot AI review requested due to automatic review settings November 17, 2025 17:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread anymap/maplibre.py Outdated
Comment thread anymap/maplibre.py Outdated
Comment thread anymap/maplibre.py Outdated
Comment thread anymap/maplibre.py Outdated
Comment thread anymap/utils.py
return os.path.abspath(output)


def geojson_bounds(geojson: dict) -> Optional[list]:
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Nov 17, 2025

@github-actions github-actions Bot temporarily deployed to pull request November 17, 2025 17:32 Inactive
@giswqs
Copy link
Copy Markdown
Member Author

giswqs commented Nov 17, 2025

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

giswqs and others added 4 commits November 17, 2025 13:31
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>
@github-actions github-actions Bot temporarily deployed to pull request November 17, 2025 18:33 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 17, 2025 18:57 Inactive
@giswqs
Copy link
Copy Markdown
Member Author

giswqs commented Nov 17, 2025

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

@github-actions github-actions Bot temporarily deployed to pull request November 18, 2025 17:07 Inactive
@giswqs
Copy link
Copy Markdown
Member Author

giswqs commented Nov 18, 2025

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

@github-actions github-actions Bot temporarily deployed to pull request November 18, 2025 17:15 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 18, 2025 19:44 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 05:57 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 06:06 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 06:35 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 13:18 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 13:52 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 14:11 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 19, 2025 18:28 Inactive
@giswqs
Copy link
Copy Markdown
Member Author

giswqs commented Nov 19, 2025

Added split and merge mode.

image

@giswqs giswqs merged commit 2c58761 into main Nov 19, 2025
8 checks passed
@giswqs giswqs deleted the vector-editor branch November 19, 2025 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants