Skip to content

Commit c6678fe

Browse files
Add support for editing vector data (#1060)
* Add support for editing vector data * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add support for parquet --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9470e21 commit c6678fe

6 files changed

Lines changed: 588 additions & 1 deletion

File tree

docs/maplibre/edit_vector.ipynb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/edit_vector.ipynb)\n",
8+
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/edit_vector.ipynb)\n",
9+
"[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n",
10+
"\n",
11+
"**Edit Vector Data Interactively**\n",
12+
"\n",
13+
"Uncomment the following line to install [leafmap](https://leafmap.org) if needed."
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"metadata": {},
20+
"outputs": [],
21+
"source": [
22+
"# %pip install \"leafmap[maplibre]\""
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"metadata": {},
29+
"outputs": [],
30+
"source": [
31+
"import leafmap.maplibregl as leafmap"
32+
]
33+
},
34+
{
35+
"cell_type": "code",
36+
"execution_count": null,
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"m = leafmap.Map()\n",
41+
"m.add_basemap(\"Google Satellite\")\n",
42+
"url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/naip_train_buildings.geojson\"\n",
43+
"properties = {\n",
44+
" \"class\": [\"apartments\", \"terrace\", \"detached\", \"house\", \"shed\", None],\n",
45+
" \"height\": 0.0,\n",
46+
"}\n",
47+
"widget = leafmap.edit_vector_data(m, url, properties=properties)\n",
48+
"m.add_layer_control()\n",
49+
"widget"
50+
]
51+
},
52+
{
53+
"cell_type": "markdown",
54+
"metadata": {},
55+
"source": [
56+
"![image](https://github.com/user-attachments/assets/c86f78ee-1f50-418e-981e-b01fe61b2b53)"
57+
]
58+
}
59+
],
60+
"metadata": {
61+
"kernelspec": {
62+
"display_name": "Python 3 (ipykernel)",
63+
"language": "python",
64+
"name": "python3"
65+
},
66+
"language_info": {
67+
"codemirror_mode": {
68+
"name": "ipython",
69+
"version": 3
70+
},
71+
"file_extension": ".py",
72+
"mimetype": "text/x-python",
73+
"name": "python",
74+
"nbconvert_exporter": "python",
75+
"pygments_lexer": "ipython3",
76+
"version": "3.12.2"
77+
}
78+
},
79+
"nbformat": 4,
80+
"nbformat_minor": 4
81+
}

docs/maplibre/overview.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ Use the draw control to draw features on the map.
255255

256256
[![](https://i.imgur.com/w8UFssd.png)](https://leafmap.org/maplibre/draw_features)
257257

258+
## Edit vector data interactively
259+
260+
Edit existing vector data interactively on a map.
261+
262+
[![](https://github.com/user-attachments/assets/c86f78ee-1f50-418e-981e-b01fe61b2b53)](https://leafmap.org/maplibre/edit_vector)
263+
258264
## Use a fallback image
259265

260266
Use a coalesce expression to display another image when a requested image is not available.

leafmap/common.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16601,3 +16601,167 @@ def get_overture_latest_release(patch=False) -> str:
1660116601
except KeyError as e:
1660216602
print(f"Key error: {e}")
1660316603
raise
16604+
16605+
16606+
def set_proj_lib_path(verbose=False):
16607+
"""
16608+
Set the PROJ_LIB and GDAL_DATA environment variables based on the current conda environment.
16609+
16610+
This function attempts to locate and set the correct paths for PROJ_LIB and GDAL_DATA
16611+
by checking multiple possible locations within the conda environment structure.
16612+
16613+
Args:
16614+
verbose (bool): If True, print additional information during the process.
16615+
16616+
Returns:
16617+
bool: True if both paths were set successfully, False otherwise.
16618+
"""
16619+
import sys
16620+
16621+
try:
16622+
from rasterio.env import set_gdal_config
16623+
16624+
# Get conda environment path
16625+
conda_env_path = os.environ.get("CONDA_PREFIX") or sys.prefix
16626+
16627+
# Define possible paths for PROJ_LIB
16628+
possible_proj_paths = [
16629+
os.path.join(conda_env_path, "share", "proj"),
16630+
os.path.join(conda_env_path, "Library", "share", "proj"),
16631+
os.path.join(conda_env_path, "Library", "share"),
16632+
]
16633+
16634+
# Define possible paths for GDAL_DATA
16635+
possible_gdal_paths = [
16636+
os.path.join(conda_env_path, "share", "gdal"),
16637+
os.path.join(conda_env_path, "Library", "share", "gdal"),
16638+
os.path.join(conda_env_path, "Library", "data", "gdal"),
16639+
os.path.join(conda_env_path, "Library", "share"),
16640+
]
16641+
16642+
# Set PROJ_LIB environment variable
16643+
proj_set = False
16644+
for proj_path in possible_proj_paths:
16645+
if os.path.exists(proj_path) and os.path.isdir(proj_path):
16646+
# Verify it contains projection data
16647+
if os.path.exists(os.path.join(proj_path, "proj.db")):
16648+
os.environ["PROJ_LIB"] = proj_path
16649+
if verbose:
16650+
print(f"PROJ_LIB set to: {proj_path}")
16651+
proj_set = True
16652+
break
16653+
16654+
# Set GDAL_DATA environment variable
16655+
gdal_set = False
16656+
for gdal_path in possible_gdal_paths:
16657+
if os.path.exists(gdal_path) and os.path.isdir(gdal_path):
16658+
# Verify it contains the header.dxf file or other critical GDAL files
16659+
if os.path.exists(
16660+
os.path.join(gdal_path, "header.dxf")
16661+
) or os.path.exists(os.path.join(gdal_path, "gcs.csv")):
16662+
os.environ["GDAL_DATA"] = gdal_path
16663+
if verbose:
16664+
print(f"GDAL_DATA set to: {gdal_path}")
16665+
gdal_set = True
16666+
break
16667+
16668+
# If paths still not found, try a last-resort approach
16669+
if not proj_set or not gdal_set:
16670+
# Try a deep search in the conda environment
16671+
for root, dirs, files in os.walk(conda_env_path):
16672+
if not gdal_set and "header.dxf" in files:
16673+
os.environ["GDAL_DATA"] = root
16674+
if verbose:
16675+
print(f"GDAL_DATA set to: {root} (deep search)")
16676+
gdal_set = True
16677+
16678+
if not proj_set and "proj.db" in files:
16679+
os.environ["PROJ_LIB"] = root
16680+
if verbose:
16681+
print(f"PROJ_LIB set to: {root} (deep search)")
16682+
proj_set = True
16683+
16684+
if proj_set and gdal_set:
16685+
break
16686+
16687+
set_gdal_config("PROJ_LIB", os.environ["PROJ_LIB"])
16688+
set_gdal_config("GDAL_DATA", os.environ["GDAL_DATA"])
16689+
16690+
except Exception as e:
16691+
print(f"Error setting projection library paths: {e}")
16692+
return
16693+
16694+
16695+
def read_vector(source, layer=None, **kwargs):
16696+
"""Reads vector data from various formats including GeoParquet.
16697+
16698+
This function dynamically determines the file type based on extension
16699+
and reads it into a GeoDataFrame. It supports both local files and HTTP/HTTPS URLs.
16700+
16701+
Args:
16702+
source: String path to the vector file or URL.
16703+
layer: String or integer specifying which layer to read from multi-layer
16704+
files (only applicable for formats like GPKG, GeoJSON, etc.).
16705+
Defaults to None.
16706+
**kwargs: Additional keyword arguments to pass to the underlying reader.
16707+
16708+
Returns:
16709+
geopandas.GeoDataFrame: A GeoDataFrame containing the vector data.
16710+
16711+
Raises:
16712+
ValueError: If the file format is not supported or source cannot be accessed.
16713+
16714+
Examples:
16715+
Read a local shapefile
16716+
>>> gdf = read_vector("path/to/data.shp")
16717+
>>>
16718+
Read a GeoParquet file from URL
16719+
>>> gdf = read_vector("https://example.com/data.parquet")
16720+
>>>
16721+
Read a specific layer from a GeoPackage
16722+
>>> gdf = read_vector("path/to/data.gpkg", layer="layer_name")
16723+
"""
16724+
16725+
import urllib.parse
16726+
16727+
import fiona
16728+
16729+
# Determine if source is a URL or local file
16730+
parsed_url = urllib.parse.urlparse(source)
16731+
is_url = parsed_url.scheme in ["http", "https"]
16732+
16733+
# If it's a local file, check if it exists
16734+
if not is_url and not os.path.exists(source):
16735+
raise ValueError(f"File does not exist: {source}")
16736+
16737+
# Get file extension
16738+
_, ext = os.path.splitext(source)
16739+
ext = ext.lower()
16740+
16741+
# Handle GeoParquet files
16742+
if ext in [".parquet", ".pq", ".geoparquet"]:
16743+
return gpd.read_parquet(source, **kwargs)
16744+
16745+
# Handle common vector formats
16746+
if ext in [".shp", ".geojson", ".json", ".gpkg", ".gml", ".kml", ".gpx"]:
16747+
# For formats that might have multiple layers
16748+
if ext in [".gpkg", ".gml"] and layer is not None:
16749+
return gpd.read_file(source, layer=layer, **kwargs)
16750+
return gpd.read_file(source, **kwargs)
16751+
16752+
# Try to use fiona to identify valid layers for formats that might have them
16753+
# Only attempt this for local files as fiona.listlayers might not work with URLs
16754+
if layer is None and ext in [".gpkg", ".gml"] and not is_url:
16755+
try:
16756+
layers = fiona.listlayers(source)
16757+
if layers:
16758+
return gpd.read_file(source, layer=layers[0], **kwargs)
16759+
except Exception:
16760+
# If listing layers fails, we'll fall through to the generic read attempt
16761+
pass
16762+
16763+
# For other formats or when layer listing fails, attempt to read using GeoPandas
16764+
try:
16765+
return gpd.read_file(source, **kwargs)
16766+
except Exception as e:
16767+
raise ValueError(f"Could not read from source '{source}': {str(e)}")

0 commit comments

Comments
 (0)