diff --git a/docs/notebooks/107_quackosm.ipynb b/docs/notebooks/107_quackosm.ipynb new file mode 100644 index 0000000000..08e6dc1fc7 --- /dev/null +++ b/docs/notebooks/107_quackosm.ipynb @@ -0,0 +1,462 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=notebooks/107_quackosm.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/notebooks/107_quackosm.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Downloading and visualizing OpenStreetMap data with QuackOSM**\n", + "\n", + "[QuackOSM](https://github.com/kraina-ai/quackosm) is a high-performance library for reading OpenStreetMap data using DuckDB. It offers significant speed improvements over traditional methods and can handle large datasets efficiently.\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) and [quackosm](https://github.com/kraina-ai/quackosm) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U leafmap quackosm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import leafmap\n", + "import leafmap.osm as osm" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Download OSM data by place name\n", + "\n", + "The `quackosm_gdf_from_place` function downloads OSM data for a given place name. QuackOSM automatically finds and downloads the required PBF files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Download all OSM features for Monaco\n", + "gdf = osm.quackosm_gdf_from_place(\"Monaco\")\n", + "print(f\"Downloaded {len(gdf)} features\")\n", + "gdf.head()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "Visualize the data on an interactive map:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map()\n", + "m.add_gdf(gdf, layer_name=\"Monaco OSM\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Filter by OSM tags\n", + "\n", + "You can filter OSM features by specifying tags. For example, download only buildings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "# Download only buildings in Monaco\n", + "buildings = osm.quackosm_gdf_from_place(\"Monaco\", tags={\"building\": True})\n", + "print(f\"Downloaded {len(buildings)} buildings\")\n", + "buildings.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map()\n", + "m.add_gdf(buildings, layer_name=\"Buildings\", style={\"color\": \"red\", \"fillOpacity\": 0.3})\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Download roads and highways:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "# Download roads in Monaco\n", + "roads = osm.quackosm_gdf_from_place(\"Monaco\", tags={\"highway\": True})\n", + "print(f\"Downloaded {len(roads)} road features\")\n", + "roads.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map()\n", + "m.add_gdf(roads, layer_name=\"Roads\", style={\"color\": \"blue\", \"weight\": 2})\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "Filter for specific tag values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "# Download restaurants and cafes\n", + "dining = osm.quackosm_gdf_from_place(\n", + " \"Monaco\", tags={\"amenity\": [\"restaurant\", \"cafe\", \"bar\"]}\n", + ")\n", + "print(f\"Downloaded {len(dining)} dining establishments\")\n", + "dining.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map()\n", + "m.add_gdf(dining, layer_name=\"Dining\", style={\"color\": \"green\", \"fillColor\": \"green\"})\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Download OSM data by bounding box\n", + "\n", + "Use `quackosm_gdf_from_bbox` to download data for a specific bounding box:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# Bounding box for Vatican City area (west, south, east, north)\n", + "bbox = (12.445, 41.900, 12.460, 41.910)\n", + "\n", + "# Download all features in the bounding box\n", + "gdf_bbox = osm.quackosm_gdf_from_bbox(bbox)\n", + "print(f\"Downloaded {len(gdf_bbox)} features\")\n", + "gdf_bbox.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[41.905, 12.4525], zoom=16)\n", + "m.add_gdf(gdf_bbox, layer_name=\"Vatican Area OSM\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "Download only buildings in the bounding box:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "buildings_bbox = osm.quackosm_gdf_from_bbox(bbox, tags={\"building\": True})\n", + "print(f\"Downloaded {len(buildings_bbox)} buildings\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[41.905, 12.4525], zoom=16)\n", + "m.add_gdf(\n", + " buildings_bbox,\n", + " layer_name=\"Buildings\",\n", + " style={\"color\": \"orange\", \"fillOpacity\": 0.5},\n", + ")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Download OSM data by geometry\n", + "\n", + "Use `quackosm_gdf_from_geometry` to download data for a custom geometry:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "from shapely.geometry import Polygon\n", + "\n", + "# Create a custom polygon\n", + "coords = [(7.41, 43.73), (7.43, 43.73), (7.43, 43.75), (7.41, 43.75), (7.41, 43.73)]\n", + "polygon = Polygon(coords)\n", + "\n", + "# Download natural features\n", + "natural = osm.quackosm_gdf_from_geometry(polygon, tags={\"natural\": True})\n", + "print(f\"Downloaded {len(natural)} natural features\")\n", + "natural.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[43.74, 7.42], zoom=14)\n", + "m.add_gdf(\n", + " natural, layer_name=\"Natural Features\", style={\"color\": \"green\", \"fillOpacity\": 0.4}\n", + ")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "## Using WKT or GeoJSON geometry\n", + "\n", + "You can also use WKT strings or GeoJSON dictionaries as geometry input:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "# Using WKT string\n", + "wkt = \"POLYGON ((7.41 43.73, 7.43 43.73, 7.43 43.75, 7.41 43.75, 7.41 43.73))\"\n", + "leisure = osm.quackosm_gdf_from_geometry(wkt, tags={\"leisure\": True})\n", + "print(f\"Downloaded {len(leisure)} leisure features\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[43.74, 7.42], zoom=14)\n", + "m.add_gdf(leisure, layer_name=\"Leisure\", style={\"color\": \"purple\", \"fillOpacity\": 0.4})\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "## Multiple layers on a single map\n", + "\n", + "Let's combine multiple OSM layers on a single map:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "# Download different feature types for Monaco\n", + "buildings = osm.quackosm_gdf_from_place(\"Monaco\", tags={\"building\": True})\n", + "roads = osm.quackosm_gdf_from_place(\"Monaco\", tags={\"highway\": True})\n", + "water = osm.quackosm_gdf_from_place(\"Monaco\", tags={\"natural\": \"water\"})\n", + "\n", + "print(f\"Buildings: {len(buildings)}, Roads: {len(roads)}, Water: {len(water)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map()\n", + "\n", + "# Add layers with different styles\n", + "if len(buildings) > 0:\n", + " m.add_gdf(\n", + " buildings,\n", + " layer_name=\"Buildings\",\n", + " style={\"color\": \"red\", \"fillOpacity\": 0.3, \"weight\": 1},\n", + " )\n", + "if len(roads) > 0:\n", + " m.add_gdf(roads, layer_name=\"Roads\", style={\"color\": \"gray\", \"weight\": 2})\n", + "if len(water) > 0:\n", + " m.add_gdf(water, layer_name=\"Water\", style={\"color\": \"blue\", \"fillOpacity\": 0.5})\n", + "\n", + "m.zoom_to_gdf(buildings)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "## Export to GeoParquet\n", + "\n", + "QuackOSM can efficiently export data to GeoParquet format, which is optimized for cloud storage and analytical workflows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "# Export Monaco buildings to GeoParquet\n", + "# output_path = osm.quackosm_to_parquet(\n", + "# \"Monaco\",\n", + "# \"monaco_buildings.parquet\",\n", + "# tags={\"building\": True}\n", + "# )\n", + "# print(f\"Saved to: {output_path}\")" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "## Common OSM tags\n", + "\n", + "Here are some commonly used OSM tags for filtering:\n", + "\n", + "| Category | Tag Examples |\n", + "|----------|-------------|\n", + "| Buildings | `{\"building\": True}`, `{\"building\": \"residential\"}` |\n", + "| Roads | `{\"highway\": True}`, `{\"highway\": [\"primary\", \"secondary\"]}` |\n", + "| Water | `{\"natural\": \"water\"}`, `{\"waterway\": True}` |\n", + "| Land use | `{\"landuse\": True}`, `{\"landuse\": [\"residential\", \"commercial\"]}` |\n", + "| Amenities | `{\"amenity\": True}`, `{\"amenity\": [\"restaurant\", \"hospital\"]}` |\n", + "| Shops | `{\"shop\": True}`, `{\"shop\": [\"supermarket\", \"convenience\"]}` |\n", + "| Tourism | `{\"tourism\": True}`, `{\"tourism\": [\"hotel\", \"attraction\"]}` |\n", + "| Natural | `{\"natural\": True}`, `{\"natural\": [\"tree\", \"wood\"]}` |\n", + "| Leisure | `{\"leisure\": True}`, `{\"leisure\": [\"park\", \"playground\"]}` |\n", + "\n", + "For a complete list of OSM tags, visit: https://wiki.openstreetmap.org/wiki/Map_features" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## Performance comparison\n", + "\n", + "QuackOSM typically offers 2-10x faster performance compared to osmnx for large datasets, thanks to:\n", + "- DuckDB-based parallel processing\n", + "- Efficient caching of downloaded PBF files\n", + "- Optimized geometry operations\n", + "\n", + "For very large areas, consider using the `verbosity_mode=\"verbose\"` parameter to see progress information." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leafmap/osm.py b/leafmap/osm.py index 4e678b8f17..26db0a69b3 100644 --- a/leafmap/osm.py +++ b/leafmap/osm.py @@ -453,3 +453,404 @@ def osm_tags_list() -> dict: import webbrowser webbrowser.open_new_tab("https://wiki.openstreetmap.org/wiki/Map_features") + + +# ============================================================================= +# QuackOSM functions - High-performance OSM data download using DuckDB +# ============================================================================= + + +def quackosm_gdf_from_place( + query: str, + tags: Optional[Dict] = None, + osm_extract_source: Optional[str] = None, + verbosity_mode: Optional[str] = "transient", + **kwargs, +) -> gpd.GeoDataFrame: + """Download OSM data for a place name using QuackOSM. + + QuackOSM is a high-performance library for reading OpenStreetMap data using DuckDB. + It automatically downloads the required PBF files and converts them to GeoDataFrame. + + Args: + query (str): Place name to geocode (e.g., "Vatican City", "Monaco", "Manhattan, New York"). + tags (dict, optional): Dict of tags used for filtering OSM features. The dict keys + should be OSM tags (e.g., building, landuse, highway, etc) and the dict values + should be either True to retrieve all items with the given tag, or a string to + get a single tag-value combination, or a list of strings to get multiple values + for the given tag. For example, tags = {'building': True} would return all + building footprints in the area. tags = {'amenity': True, 'highway': 'bus_stop'} + would return all amenities and highway=bus_stop features. Defaults to None (all features). + osm_extract_source (str, optional): Source for OSM extracts. Options include "geofabrik", + "osmfr", "bbbike", etc. If None, QuackOSM will automatically select the best source. + Defaults to None. + verbosity_mode (str, optional): Verbosity mode for progress output. Options are + "verbose", "transient", or "silent". Defaults to "transient". + **kwargs: Additional keyword arguments passed to QuackOSM's convert_osm_extract_to_geodataframe + or convert_geometry_to_geodataframe functions. + + Returns: + GeoDataFrame: A GeoDataFrame containing OSM features with 'tags' and 'geometry' columns. + + Example: + >>> import leafmap.osm as osm + >>> # Download all OSM features for Monaco + >>> gdf = osm.quackosm_gdf_from_place("Monaco") + >>> # Download only buildings + >>> gdf = osm.quackosm_gdf_from_place("Monaco", tags={"building": True}) + >>> # Download amenities and shops + >>> gdf = osm.quackosm_gdf_from_place("Monaco", tags={"amenity": True, "shop": True}) + """ + check_package("quackosm", "https://github.com/kraina-ai/quackosm") + + try: + import quackosm as qosm + from quackosm.osm_extracts import OsmExtractSource + except ImportError: + raise ImportError( + "quackosm package is required. Please install it using " + "'pip install quackosm' or 'conda install -c conda-forge quackosm'" + ) + + # Convert tags dict to QuackOSM format if provided + tags_filter = _convert_tags_to_quackosm_filter(tags) if tags else None + + # Set verbosity mode + kwargs["verbosity_mode"] = verbosity_mode + + # Try using osm_extract_source first if specified + if osm_extract_source: + source = getattr( + OsmExtractSource, osm_extract_source.upper(), osm_extract_source + ) + gdf = qosm.convert_osm_extract_to_geodataframe( + query, osm_extract_source=source, tags_filter=tags_filter, **kwargs + ) + else: + # Try convert_osm_extract_to_geodataframe first, fall back to geometry-based approach + try: + gdf = qosm.convert_osm_extract_to_geodataframe( + query, tags_filter=tags_filter, **kwargs + ) + except Exception: + # Fall back to geocoding + geometry-based download + geometry = qosm.geocode_to_geometry(query) + gdf = qosm.convert_geometry_to_geodataframe( + geometry, tags_filter=tags_filter, **kwargs + ) + + return gdf + + +def quackosm_gdf_from_bbox( + bbox: Union[Tuple[float, float, float, float], List[float]], + tags: Optional[Dict] = None, + verbosity_mode: Optional[str] = "transient", + **kwargs, +) -> gpd.GeoDataFrame: + """Download OSM data for a bounding box using QuackOSM. + + QuackOSM is a high-performance library for reading OpenStreetMap data using DuckDB. + It automatically downloads the required PBF files and converts them to GeoDataFrame. + + Args: + bbox (tuple or list): Bounding box as (west, south, east, north) or + (minx, miny, maxx, maxy) in WGS84 coordinates. + tags (dict, optional): Dict of tags used for filtering OSM features. The dict keys + should be OSM tags (e.g., building, landuse, highway, etc) and the dict values + should be either True to retrieve all items with the given tag, or a string to + get a single tag-value combination, or a list of strings to get multiple values + for the given tag. Defaults to None (all features). + verbosity_mode (str, optional): Verbosity mode for progress output. Options are + "verbose", "transient", or "silent". Defaults to "transient". + **kwargs: Additional keyword arguments passed to QuackOSM's convert_geometry_to_geodataframe. + + Returns: + GeoDataFrame: A GeoDataFrame containing OSM features with 'tags' and 'geometry' columns. + + Example: + >>> import leafmap.osm as osm + >>> # Download all OSM features in a bounding box (Monaco) + >>> bbox = (7.409, 43.724, 7.439, 43.752) # west, south, east, north + >>> gdf = osm.quackosm_gdf_from_bbox(bbox) + >>> # Download only roads + >>> gdf = osm.quackosm_gdf_from_bbox(bbox, tags={"highway": True}) + """ + check_package("quackosm", "https://github.com/kraina-ai/quackosm") + + try: + import quackosm as qosm + from shapely.geometry import box + except ImportError: + raise ImportError( + "quackosm package is required. Please install it using " + "'pip install quackosm' or 'conda install -c conda-forge quackosm'" + ) + + # Create geometry from bbox (west, south, east, north) + west, south, east, north = bbox + geometry = box(west, south, east, north) + + # Convert tags dict to QuackOSM format if provided + tags_filter = _convert_tags_to_quackosm_filter(tags) if tags else None + + # Set verbosity mode + kwargs["verbosity_mode"] = verbosity_mode + + gdf = qosm.convert_geometry_to_geodataframe( + geometry, tags_filter=tags_filter, **kwargs + ) + + return gdf + + +def quackosm_gdf_from_geometry( + geometry, + tags: Optional[Dict] = None, + verbosity_mode: Optional[str] = "transient", + **kwargs, +) -> gpd.GeoDataFrame: + """Download OSM data for a geometry using QuackOSM. + + QuackOSM is a high-performance library for reading OpenStreetMap data using DuckDB. + It automatically downloads the required PBF files and converts them to GeoDataFrame. + + Args: + geometry: A Shapely geometry (Polygon, MultiPolygon), WKT string, GeoJSON dict, + or a GeoDataFrame. The geometry defines the area of interest. + tags (dict, optional): Dict of tags used for filtering OSM features. The dict keys + should be OSM tags (e.g., building, landuse, highway, etc) and the dict values + should be either True to retrieve all items with the given tag, or a string to + get a single tag-value combination, or a list of strings to get multiple values + for the given tag. Defaults to None (all features). + verbosity_mode (str, optional): Verbosity mode for progress output. Options are + "verbose", "transient", or "silent". Defaults to "transient". + **kwargs: Additional keyword arguments passed to QuackOSM's convert_geometry_to_geodataframe. + + Returns: + GeoDataFrame: A GeoDataFrame containing OSM features with 'tags' and 'geometry' columns. + + Example: + >>> import leafmap.osm as osm + >>> from shapely.geometry import box + >>> # Download OSM features for a custom geometry + >>> geometry = box(7.41, 43.73, 7.43, 43.75) # Monaco area + >>> gdf = osm.quackosm_gdf_from_geometry(geometry) + >>> # Download only water features + >>> gdf = osm.quackosm_gdf_from_geometry(geometry, tags={"natural": "water"}) + """ + check_package("quackosm", "https://github.com/kraina-ai/quackosm") + + try: + import quackosm as qosm + from shapely import wkt + from shapely.geometry import shape + except ImportError: + raise ImportError( + "quackosm package is required. Please install it using " + "'pip install quackosm' or 'conda install -c conda-forge quackosm'" + ) + + # Convert various input types to Shapely geometry + if isinstance(geometry, str): + # Assume WKT string + geometry = wkt.loads(geometry) + elif isinstance(geometry, dict): + # Assume GeoJSON dict + geometry = shape(geometry) + elif isinstance(geometry, gpd.GeoDataFrame): + # Get the union of all geometries in the GeoDataFrame + geometry = geometry.geometry.unary_union + + # Convert tags dict to QuackOSM format if provided + tags_filter = _convert_tags_to_quackosm_filter(tags) if tags else None + + # Set verbosity mode + kwargs["verbosity_mode"] = verbosity_mode + + gdf = qosm.convert_geometry_to_geodataframe( + geometry, tags_filter=tags_filter, **kwargs + ) + + return gdf + + +def quackosm_gdf_from_pbf( + pbf_path: str, + tags: Optional[Dict] = None, + geometry=None, + verbosity_mode: Optional[str] = "transient", + **kwargs, +) -> gpd.GeoDataFrame: + """Load OSM data from a local PBF file using QuackOSM. + + QuackOSM is a high-performance library for reading OpenStreetMap data using DuckDB. + This function reads a local PBF file and converts it to a GeoDataFrame. + + Args: + pbf_path (str): Path to the local PBF file. + tags (dict, optional): Dict of tags used for filtering OSM features. The dict keys + should be OSM tags (e.g., building, landuse, highway, etc) and the dict values + should be either True to retrieve all items with the given tag, or a string to + get a single tag-value combination, or a list of strings to get multiple values + for the given tag. Defaults to None (all features). + geometry: Optional Shapely geometry to clip the data to. Defaults to None. + verbosity_mode (str, optional): Verbosity mode for progress output. Options are + "verbose", "transient", or "silent". Defaults to "transient". + **kwargs: Additional keyword arguments passed to QuackOSM's convert_pbf_to_geodataframe. + + Returns: + GeoDataFrame: A GeoDataFrame containing OSM features with 'tags' and 'geometry' columns. + + Example: + >>> import leafmap.osm as osm + >>> # Load OSM data from a PBF file + >>> gdf = osm.quackosm_gdf_from_pbf("monaco.osm.pbf") + >>> # Load only buildings + >>> gdf = osm.quackosm_gdf_from_pbf("monaco.osm.pbf", tags={"building": True}) + """ + check_package("quackosm", "https://github.com/kraina-ai/quackosm") + + try: + import quackosm as qosm + except ImportError: + raise ImportError( + "quackosm package is required. Please install it using " + "'pip install quackosm' or 'conda install -c conda-forge quackosm'" + ) + + # Convert tags dict to QuackOSM format if provided + tags_filter = _convert_tags_to_quackosm_filter(tags) if tags else None + + # Set verbosity mode + kwargs["verbosity_mode"] = verbosity_mode + + # Add geometry filter if provided + if geometry is not None: + kwargs["geometry_filter"] = geometry + + gdf = qosm.convert_pbf_to_geodataframe(pbf_path, tags_filter=tags_filter, **kwargs) + + return gdf + + +def _convert_tags_to_quackosm_filter(tags: Dict) -> Dict: + """Convert osmnx-style tags dict to QuackOSM filter format. + + QuackOSM uses a similar but slightly different format for tag filtering. + This function converts the osmnx-style dict to QuackOSM format. + + Args: + tags (dict): Dict with OSM tags as keys and True, string, or list of strings as values. + + Returns: + dict: Tags filter in QuackOSM format. + """ + if tags is None: + return None + + # QuackOSM expects the same format as osmnx: + # {key: True} - match any value for the key + # {key: value} - match specific value + # {key: [value1, value2]} - match any of the values + quackosm_filter = {} + + for key, value in tags.items(): + if value is True: + # Match any value for this key + quackosm_filter[key] = True + elif isinstance(value, str): + # Match specific value + quackosm_filter[key] = value + elif isinstance(value, list): + # Match any of these values + quackosm_filter[key] = value + else: + quackosm_filter[key] = value + + return quackosm_filter + + +def quackosm_to_parquet( + source: Union[str, Tuple, gpd.GeoDataFrame], + output_path: str, + tags: Optional[Dict] = None, + verbosity_mode: Optional[str] = "transient", + **kwargs, +) -> str: + """Download OSM data and save to GeoParquet format using QuackOSM. + + QuackOSM can efficiently save OSM data directly to GeoParquet format, + which is optimized for cloud storage and analytical workflows. + + Args: + source: The data source. Can be: + - A place name string (e.g., "Monaco") + - A bounding box tuple (west, south, east, north) + - A Shapely geometry + - A GeoDataFrame + output_path (str): Path to save the output GeoParquet file. + tags (dict, optional): Dict of tags used for filtering OSM features. Defaults to None. + verbosity_mode (str, optional): Verbosity mode for progress output. Defaults to "transient". + **kwargs: Additional keyword arguments passed to QuackOSM functions. + + Returns: + str: Path to the saved GeoParquet file. + + Example: + >>> import leafmap.osm as osm + >>> # Save Monaco buildings to GeoParquet + >>> path = osm.quackosm_to_parquet("Monaco", "monaco_buildings.parquet", tags={"building": True}) + """ + check_package("quackosm", "https://github.com/kraina-ai/quackosm") + + try: + import quackosm as qosm + from shapely.geometry import box + except ImportError: + raise ImportError( + "quackosm package is required. Please install it using " + "'pip install quackosm' or 'conda install -c conda-forge quackosm'" + ) + + # Convert tags dict to QuackOSM format if provided + tags_filter = _convert_tags_to_quackosm_filter(tags) if tags else None + + # Set verbosity mode + kwargs["verbosity_mode"] = verbosity_mode + + # Determine source type and convert + if isinstance(source, str): + # Place name - try extract first, then geometry + try: + parquet_path = qosm.convert_osm_extract_to_parquet( + source, result_file_path=output_path, tags_filter=tags_filter, **kwargs + ) + except Exception: + geometry = qosm.geocode_to_geometry(source) + parquet_path = qosm.convert_geometry_to_parquet( + geometry, + result_file_path=output_path, + tags_filter=tags_filter, + **kwargs, + ) + elif isinstance(source, (tuple, list)) and len(source) == 4: + # Bounding box + west, south, east, north = source + geometry = box(west, south, east, north) + parquet_path = qosm.convert_geometry_to_parquet( + geometry, result_file_path=output_path, tags_filter=tags_filter, **kwargs + ) + elif isinstance(source, gpd.GeoDataFrame): + # GeoDataFrame + geometry = source.geometry.unary_union + parquet_path = qosm.convert_geometry_to_parquet( + geometry, result_file_path=output_path, tags_filter=tags_filter, **kwargs + ) + else: + # Assume Shapely geometry + parquet_path = qosm.convert_geometry_to_parquet( + source, result_file_path=output_path, tags_filter=tags_filter, **kwargs + ) + + return str(parquet_path)