diff --git a/docs/maplibre/create_vector.ipynb b/docs/maplibre/create_vector.ipynb new file mode 100644 index 0000000000..f2f4cd356c --- /dev/null +++ b/docs/maplibre/create_vector.ipynb @@ -0,0 +1,124 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/create_vector.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/create_vector.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Create Vector Data Interactively**\n", + "\n", + "This notebook demonstrates how to create vector data interactively using the `leafmap` Python package.\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"leafmap[maplibre]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import leafmap.maplibregl as leafmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create an interactive map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[-74.1935, 40.6681], zoom=15, style=\"liberty\")\n", + "m.add_basemap(\"Satellite\")\n", + "m.add_layer_control()\n", + "m.add_draw_control(\n", + " controls=[\"point\", \"polygon\", \"line_string\", \"trash\"], position=\"top-right\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set up default parameters for drawn features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "properties = {\n", + " \"Type\": [\"Residential\", \"Commercial\", \"Industrial\"],\n", + " \"Area\": 3000,\n", + " \"Name\": \"Building\",\n", + " \"City\": \"New York\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widget = leafmap.create_vector_data(m, properties, file_ext=\"geojson\")\n", + "widget" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the drawing tools to create vector data interactively on the map. Change the properties of the drawn features as needed. Click on the **Save** button to save the properties of the drawn features. Once you are done, click on the **Export** button to export the drawn features to a GeoJSON file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![image](https://github.com/user-attachments/assets/70518d0a-d78e-4e21-94ab-2c18a9fa8f64)\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/maplibre/overview.md b/docs/maplibre/overview.md index fb6b075c85..6b4d787d4c 100644 --- a/docs/maplibre/overview.md +++ b/docs/maplibre/overview.md @@ -212,6 +212,14 @@ Utilize and refine data from the MapTiler Countries to create a Choropleth map o [![](https://i.imgur.com/k1d6k9I.png)](https://leafmap.org/maplibre/countries_filter) + +## Create vector data + +Create vector data interactively on a map. + +[![image](https://github.com/user-attachments/assets/70518d0a-d78e-4e21-94ab-2c18a9fa8f64) +](https://leafmap.org/maplibre/create_vector) + ## Customize marker icon image Use the icon-image property to change the icon image of a marker. diff --git a/leafmap/maplibregl.py b/leafmap/maplibregl.py index 6b8ee5842f..dcc01de61b 100644 --- a/leafmap/maplibregl.py +++ b/leafmap/maplibregl.py @@ -3157,7 +3157,7 @@ def add_overture_3d_buildings( def add_overture_data( self, - release: str = "2024-10-23", + release: str = "2024-12-18", theme: str = "buildings", style: Optional[Dict[str, Any]] = None, visible: bool = True, @@ -3170,7 +3170,7 @@ def add_overture_data( Args: release (str, optional): The release date of the data. Defaults to - "2024-10-23". For more info, see https://github.com/OvertureMaps/overture-tiles + "2024-12-28". For more info, see https://github.com/OvertureMaps/overture-tiles theme (str, optional): The theme of the data. It can be one of the following: "addresses", "base", "buildings", "divisions", "places", "transportation". Defaults to "buildings". @@ -3399,7 +3399,7 @@ def add_overture_data( def add_overture_buildings( self, - release: str = "2024-10-23", + release: str = "2024-12-18", style: Optional[Dict[str, Any]] = None, type: str = "line", visible: bool = True, @@ -3412,7 +3412,7 @@ def add_overture_buildings( Args: release (str, optional): The release date of the data. Defaults to - "2024-10-23". For more info, see https://github.com/OvertureMaps/overture-tiles + "2024-12-18". For more info, see https://github.com/OvertureMaps/overture-tiles style (Optional[Dict[str, Any]], optional): The style dictionary for the data. Defaults to None. type (str, optional): The type of the data. It can be "line" or "fill". @@ -5203,6 +5203,223 @@ def on_change(change): return main_widget +def create_vector_data( + m: Optional[Map] = None, + properties: Optional[Dict[str, List[Any]]] = None, + time_format: str = "%Y%m%dT%H%M%S", + column_widths: Optional[List[int]] = (9, 3), + map_height: str = "600px", + out_dir: Optional[str] = None, + filename_prefix: str = "", + file_ext: str = "geojson", + **kwargs: Any, +) -> widgets.VBox: + """Generates a widget-based interface for creating and managing vector data on a map. + + This function creates an interactive widget interface that allows users to draw features + (points, lines, polygons) on a map, assign properties to these features, and export them + as GeoJSON files. The interface includes a map, a sidebar for property management, and + buttons for saving, exporting, and resetting the data. + + Args: + m (Map, optional): An existing Map object. If not provided, a default map with + basemaps and drawing controls will be created. Defaults to None. + properties (Dict[str, List[Any]], optional): A dictionary where keys are property names + and values are lists of possible values for each property. These properties can be + assigned to the drawn features. Defaults to None. + time_format (str, optional): The format string for the timestamp used in the exported + filename. Defaults to "%Y%m%dT%H%M%S". + column_widths (Optional[List[int]], optional): A list of two integers specifying the + relative widths of the map and sidebar columns. Defaults to (9, 3). + map_height (str, optional): The height of the map widget. Defaults to "600px". + out_dir (str, optional): The directory where the exported GeoJSON files will be saved. + If not provided, the current working directory is used. Defaults to None. + filename_prefix (str, optional): A prefix to be added to the exported filename. + Defaults to "". + file_ext (str, optional): The file extension for the exported file. Defaults to "geojson". + **kwargs (Any): Additional keyword arguments that may be passed to the function. + + Returns: + widgets.VBox: A vertical box widget containing the map, sidebar, and control buttons. + + Example: + >>> properties = { + ... "Type": ["Residential", "Commercial", "Industrial"], + ... "Area": [100, 200, 300], + ... } + >>> widget = create_vector_data(properties=properties) + >>> display(widget) # Display the widget in a Jupyter notebook + """ + from datetime import datetime + + main_widget = widgets.VBox() + output = widgets.Output() + + if out_dir is None: + out_dir = os.getcwd() + + def create_default_map(): + m = Map(style="liberty", height=map_height) + m.add_basemap("Satellite") + m.add_basemap("OpenStreetMap.Mapnik", visible=True) + m.add_overture_buildings(visible=True) + m.add_overture_data(theme="transportation") + m.add_layer_control() + m.add_draw_control( + controls=["point", "polygon", "line_string", "trash"], position="top-right" + ) + return m + + if m is None: + m = create_default_map() + + setattr(m, "draw_features", {}) + + sidebar_widget = widgets.VBox() + + prop_widgets = widgets.VBox() + + if isinstance(properties, dict): + for key, values in properties.items(): + + if isinstance(values, list) or isinstance(values, tuple): + prop_widget = widgets.Dropdown( + options=values, + # value=None, + description=key, + ) + prop_widgets.children += (prop_widget,) + elif isinstance(values, int): + prop_widget = widgets.IntText( + value=values, + description=key, + ) + prop_widgets.children += (prop_widget,) + elif isinstance(values, float): + prop_widget = widgets.FloatText( + value=values, + description=key, + ) + prop_widgets.children += (prop_widget,) + else: + prop_widget = widgets.Text( + value=values, + description=key, + ) + prop_widgets.children += (prop_widget,) + + def draw_change(lng_lat): + if lng_lat.new: + if len(m.draw_features_selected) > 0: + feature_id = m.draw_features_selected[0]["id"] + if feature_id not in m.draw_features: + m.draw_features[feature_id] = {} + for key, values in properties.items(): + if isinstance(values, list) or isinstance(values, tuple): + m.draw_features[feature_id][key] = values[0] + else: + m.draw_features[feature_id][key] = values + else: + for prop_widget in prop_widgets.children: + key = prop_widget.description + prop_widget.value = m.draw_features[feature_id][key] + + else: + for prop_widget in prop_widgets.children: + key = prop_widget.description + if isinstance(properties[key], list) or isinstance( + properties[key], tuple + ): + prop_widget.value = properties[key][0] + else: + prop_widget.value = properties[key] + + m.observe(draw_change, names="draw_features_selected") + + button_layout = widgets.Layout(width="97px") + save = widgets.Button( + description="Save", button_style="primary", layout=button_layout + ) + export = widgets.Button( + description="Export", button_style="primary", layout=button_layout + ) + reset = widgets.Button( + description="Reset", button_style="primary", layout=button_layout + ) + + def on_save_click(b): + + if len(m.draw_features_selected) > 0: + feature_id = m.draw_features_selected[0]["id"] + for prop_widget in prop_widgets.children: + key = prop_widget.description + m.draw_features[feature_id][key] = prop_widget.value + else: + with output: + output.clear_output() + print("Please select a feature to save.") + + save.on_click(on_save_click) + + def on_export_click(b): + current_time = datetime.now().strftime(time_format) + filename = os.path.join(out_dir, f"{filename_prefix}{current_time}.{file_ext}") + + for index, feature in enumerate(m.draw_feature_collection_all["features"]): + feature_id = feature["id"] + if feature_id in m.draw_features: + m.draw_feature_collection_all["features"][index]["properties"] = ( + m.draw_features[feature_id] + ) + + gdf = gpd.GeoDataFrame.from_features( + m.draw_feature_collection_all, crs="EPSG:4326" + ) + gdf.to_file(filename) + with output: + + print(f"Exported: {filename}") + + export.on_click(on_export_click) + + def on_reset_click(b): + output.clear_output() + for prop_widget in prop_widgets.children: + description = prop_widget.description + if description in properties: + if isinstance(properties[description], list) or isinstance( + properties[description], tuple + ): + prop_widget.value = properties[description][0] + else: + prop_widget.value = properties[description] + + reset.on_click(on_reset_click) + + sidebar_widget.children = [ + prop_widgets, + widgets.HBox([save, export, reset]), + output, + ] + + left_col_layout = v.Col( + cols=column_widths[0], + children=[m], + class_="pa-1", # padding for consistent spacing + ) + right_col_layout = v.Col( + cols=column_widths[1], + children=[sidebar_widget], + class_="pa-1", # padding for consistent spacing + ) + row1 = v.Row( + class_="d-flex flex-wrap", + children=[left_col_layout, right_col_layout], + ) + main_widget = v.Col(children=[row1]) + return main_widget + + class MapWidget(v.Row): def __init__(self, left_obj, right_obj, column_widths=(5, 1), **kwargs): diff --git a/mkdocs.yml b/mkdocs.yml index 2c88531b3e..6f3cff8253 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -181,6 +181,7 @@ nav: - maplibre/cluster.ipynb - maplibre/color_switcher.ipynb - maplibre/countries_filter.ipynb + - maplibre/create_vector.ipynb - maplibre/custom_marker.ipynb - maplibre/data_driven_lines.ipynb - maplibre/disable_scroll_zoom.ipynb