diff --git a/docs/installation.md b/docs/installation.md index dc34172bff..13244afcab 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -30,6 +30,33 @@ Optionally, you can install some [Jupyter notebook extensions](https://github.co conda install jupyter_contrib_nbextensions -c conda-forge ``` +## Optional Dependencies + +Leafmap has various optional dependencies to enable additional features. You can install them as needed: + +### Polars Support + +To enable Polars DataFrame support for modern, high-performance geospatial workflows: + +```bash +pip install "leafmap[polars]" +``` + +This enables the `add_polars()` method for visualizing Polars DataFrames with geometry columns (e.g., from Polars-ST or polars-h3). + +### Other Optional Dependencies + +Install other optional features: + +```bash +pip install "leafmap[vector]" # Vector analysis tools +pip install "leafmap[raster]" # Raster data support +pip install "leafmap[lidar]" # LiDAR data analysis +pip install "leafmap[backends]" # All mapping backends +pip install "leafmap[maplibre]" # MapLibre GL backend +pip install "leafmap[ai]" # AI/ML features +``` + ## Install from GitHub To install the development version from GitHub using [Git](https://git-scm.com/), run the following command in your terminal: diff --git a/docs/notebooks/110_polars.ipynb b/docs/notebooks/110_polars.ipynb new file mode 100644 index 0000000000..85b6dcf0eb --- /dev/null +++ b/docs/notebooks/110_polars.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Polars DataFrame Support in Leafmap\n", + "\n", + "This notebook demonstrates first-class support for Polars-based geospatial workflows in leafmap.\n", + "\n", + "## Installation\n", + "\n", + "```bash\n", + "pip install leafmap polars geopandas\n", + "```\n", + "\n", + "For Polars-ST support:\n", + "```bash\n", + "pip install polars-st\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import leafmap\n", + "import polars as pl\n", + "import geopandas as gpd\n", + "from shapely.geometry import Point, Polygon, box\n", + "from shapely import wkb" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1: Simple Point Data\n", + "\n", + "Create a Polars DataFrame with point geometries using WKT (Well-Known Text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create sample data with WKT geometries\n", + "data = {\n", + " \"city\": [\"New York\", \"Los Angeles\", \"Chicago\", \"Houston\", \"Phoenix\"],\n", + " \"population\": [8336817, 3979576, 2693976, 2320268, 1680992],\n", + " \"geometry\": [\n", + " \"POINT(-74.0060 40.7128)\", # New York\n", + " \"POINT(-118.2437 34.0522)\", # Los Angeles\n", + " \"POINT(-87.6298 41.8781)\", # Chicago\n", + " \"POINT(-95.3698 29.7604)\", # Houston\n", + " \"POINT(-112.0740 33.4484)\", # Phoenix\n", + " ],\n", + "}\n", + "\n", + "df_polars = pl.DataFrame(data)\n", + "print(df_polars)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize with leafmap\n", + "m = leafmap.Map(center=[37.0902, -95.7129], zoom=4)\n", + "m.add_polars(\n", + " df_polars,\n", + " geometry=\"geometry\",\n", + " crs=\"EPSG:4326\",\n", + " layer_name=\"US Cities\",\n", + " zoom_to_layer=True,\n", + ")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: Converting from GeoDataFrame to Polars\n", + "\n", + "Read GeoJSON/GeoParquet and work with it in Polars" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start with a GeoDataFrame\n", + "url = \"https://github.com/opengeos/datasets/releases/download/vector/cables.geojson\"\n", + "gdf = gpd.read_file(url)\n", + "\n", + "# Convert to Polars with WKB geometry\n", + "from shapely import wkb\n", + "\n", + "# Convert geometries to WKB bytes\n", + "gdf[\"geometry_wkb\"] = gdf[\"geometry\"].apply(lambda x: wkb.dumps(x))\n", + "df_polars = pl.DataFrame(gdf.drop(columns=[\"geometry\"]))\n", + "\n", + "print(df_polars.head())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize the Polars DataFrame\n", + "m = leafmap.Map()\n", + "m.add_polars(\n", + " df_polars,\n", + " geometry=\"geometry_wkb\",\n", + " crs=\"EPSG:4326\",\n", + " layer_name=\"Submarine Cables\",\n", + " zoom_to_layer=True,\n", + " style={\"color\": \"blue\", \"weight\": 2},\n", + ")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 3: Polars Data Processing Pipeline\n", + "\n", + "Demonstrate Polars-first workflow with filtering and aggregation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from shapely.geometry import box\n", + "import numpy as np\n", + "\n", + "# Create sample polygon data\n", + "\n", + "# Create grid of rectangles\n", + "geometries = []\n", + "names = []\n", + "values = []\n", + "\n", + "\n", + "for i in range(-5, 5):\n", + " for j in range(-5, 5):\n", + " geometries.append(wkb.dumps(box(i, j, i + 0.8, j + 0.8)))\n", + " names.append(f\"Cell_{i}_{j}\")\n", + " values.append(np.random.randint(0, 100))\n", + "\n", + "df_grid = pl.DataFrame({\"name\": names, \"value\": values, \"geometry\": geometries})\n", + "\n", + "print(df_grid.head())\n", + "print(f\"\\nTotal cells: {len(df_grid)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter high-value cells using Polars\n", + "df_filtered = df_grid.filter(pl.col(\"value\") > 75)\n", + "print(f\"High-value cells: {len(df_filtered)}\")\n", + "\n", + "# Visualize\n", + "m = leafmap.Map(center=[0, 0], zoom=6)\n", + "m.add_polars(\n", + " df_filtered,\n", + " geometry=\"geometry\",\n", + " crs=\"EPSG:4326\",\n", + " layer_name=\"High Value Cells\",\n", + " zoom_to_layer=True,\n", + " style={\"fillColor\": \"red\", \"fillOpacity\": 0.5, \"color\": \"darkred\"},\n", + ")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Benefits of Polars Integration\n", + "\n", + "1. **Performance**: Polars is significantly faster than Pandas for large datasets\n", + "2. **Memory Efficiency**: Better memory management with Apache Arrow backend\n", + "3. **Native Pipeline**: Stay in Polars-native workflow end-to-end\n", + "4. **Modern API**: Expressive and intuitive query syntax\n", + "5. **GeoParquet Support**: Native support for reading/writing GeoParquet files\n", + "\n", + "## Next Steps\n", + "\n", + "- Explore [Polars-ST](https://github.com/Oreilles/polars-st) for advanced spatial operations\n", + "- Try [polars-h3](https://github.com/Filimoa/polars-h3) for H3 indexing workflows\n", + "- Read GeoParquet files directly with Polars for maximum performance" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/leafmap/foliumap.py b/leafmap/foliumap.py index a482f5230a..864a845d2e 100644 --- a/leafmap/foliumap.py +++ b/leafmap/foliumap.py @@ -2068,6 +2068,150 @@ def add_gdf( **kwargs, ) + def add_polars( + self, + df, + geometry: Optional[str] = "geometry", + crs: Optional[str] = None, + layer_name: Optional[str] = "Untitled", + zoom_to_layer: Optional[bool] = True, + info_mode: Optional[str] = "on_hover", + opacity: Optional[float] = 1.0, + **kwargs, + ) -> None: + """Adds a Polars DataFrame with geometry to the map. + + This method supports Polars-ST DataFrames with spatial geometry columns + and enables direct visualization of Polars-based geospatial data without + manual conversion to GeoPandas. + + Args: + df: A Polars DataFrame with a geometry column (e.g., from Polars-ST). + geometry (str, optional): The name of the geometry column. Defaults to "geometry". + crs (str, optional): The CRS of the geometry data (e.g., "EPSG:4326"). + If None, will try to detect from Polars-ST metadata or default to EPSG:4326. + layer_name (str, optional): The layer name to be used. Defaults to "Untitled". + zoom_to_layer (bool, optional): Whether to zoom to the layer. Defaults to True. + info_mode (str, optional): Displays the attributes by either on_hover or on_click. + Any value other than "on_hover" or "on_click" will be treated as None. + Defaults to "on_hover". + opacity (float, optional): The opacity of the layer. Defaults to 1.0. + **kwargs: Additional keyword arguments to pass to add_gdf. + + Raises: + ImportError: If polars or required dependencies are not installed. + ValueError: If the specified geometry column is not found. + TypeError: If the input is not a Polars DataFrame. + + Examples: + >>> import polars as pl + >>> # With Polars-ST + >>> df = pl.read_parquet("data.geoparquet") + >>> m = leafmap.Map() + >>> m.add_polars(df, geometry="geometry", crs="EPSG:4326") + """ + try: + import polars as pl + except ImportError: + raise ImportError( + "polars is required for add_polars(). " + "Install it with: pip install polars" + ) + + try: + import geopandas as gpd + from shapely import wkb + except ImportError: + raise ImportError( + "geopandas and shapely are required. " + "Install them with: pip install geopandas shapely" + ) + + # Validate input + if not isinstance(df, pl.DataFrame): + raise TypeError( + f"Expected a Polars DataFrame, got {type(df).__name__}. " + "Use add_gdf() for GeoPandas DataFrames." + ) + + # Check if geometry column exists + if geometry not in df.columns: + raise ValueError( + f"Geometry column '{geometry}' not found. " + f"Available columns: {df.columns}" + ) + + # Convert Polars DataFrame to GeoPandas + # Strategy: Convert to pandas first, then create GeoDataFrame + pdf = df.to_pandas() + + # Handle geometry column - could be WKB binary or WKT string + geom_col = pdf[geometry] + + # Try to convert geometries + try: + # Check if it's binary (WKB from Polars-ST) + if pdf[geometry].dtype == object: + # Try WKB first (most common for Polars-ST) + try: + geometries = geom_col.apply( + lambda x: wkb.loads(bytes(x)) if x is not None else None + ) + except Exception: + # Try WKT + try: + from shapely import wkt + + geometries = geom_col.apply( + lambda x: wkt.loads(str(x)) if x is not None else None + ) + except Exception: + # Assume it's already shapely geometry + geometries = geom_col + else: + # Might be serialized as bytes + geometries = geom_col.apply( + lambda x: wkb.loads(x) if x is not None else None + ) + except Exception as e: + raise ValueError( + f"Failed to parse geometry column '{geometry}'. " + f"Expected WKB binary or WKT string format. Error: {e}" + ) + + # Drop the original geometry column and create GeoDataFrame + pdf = pdf.drop(columns=[geometry]) + gdf = gpd.GeoDataFrame(pdf, geometry=geometries) + + # Set CRS + if crs is not None: + gdf.crs = crs + elif gdf.crs is None: + # Try to detect CRS from Polars-ST metadata + # Polars-ST stores CRS in column metadata + try: + # Check if the original Polars column has metadata + if ( + hasattr(df[geometry], "_metadata") + and "crs" in df[geometry]._metadata + ): + gdf.crs = df[geometry]._metadata["crs"] + else: + # Default to EPSG:4326 + gdf.crs = "EPSG:4326" + except Exception: + gdf.crs = "EPSG:4326" + + # Now use the existing add_gdf method + self.add_gdf( + gdf, + layer_name=layer_name, + zoom_to_layer=zoom_to_layer, + info_mode=info_mode, + opacity=opacity, + **kwargs, + ) + def add_gdf_from_postgis( self, sql: str, diff --git a/leafmap/leafmap.py b/leafmap/leafmap.py index ff426c7ea5..8521116b04 100644 --- a/leafmap/leafmap.py +++ b/leafmap/leafmap.py @@ -3216,6 +3216,184 @@ def add_gdf( **kwargs, ) + def add_polars( + self, + df, + geometry: Optional[str] = "geometry", + crs: Optional[str] = None, + layer_name: Optional[str] = "Untitled", + style: Optional[dict] = {}, + hover_style: Optional[dict] = {}, + style_callback: Optional[Callable] = None, + fill_colors: Optional[List[str]] = None, + info_mode: Optional[str] = "on_hover", + zoom_to_layer: Optional[bool] = False, + encoding: Optional[str] = "utf-8", + **kwargs, + ) -> None: + """Adds a Polars DataFrame with geometry to the map. + + This method supports Polars-ST DataFrames with spatial geometry columns + and enables direct visualization of Polars-based geospatial data without + manual conversion to GeoPandas. + + Args: + df: A Polars DataFrame with a geometry column (e.g., from Polars-ST). + geometry (str, optional): The name of the geometry column. Defaults to "geometry". + crs (str, optional): The CRS of the geometry data (e.g., "EPSG:4326"). + If None, defaults to EPSG:4326. For Polars-ST DataFrames, specify the CRS explicitly. + layer_name (str, optional): The layer name to be used. Defaults to "Untitled". + style (dict, optional): A dictionary specifying the style to be used. Defaults to {}. + hover_style (dict, optional): Hover style dictionary. Defaults to {}. + style_callback (function, optional): Styling function that is called + for each feature, and should return the feature style. This + styling function takes the feature as argument. Defaults to None. + fill_colors (list, optional): The random colors to use for filling + polygons. Defaults to ["black"]. + info_mode (str, optional): Displays the attributes by either on_hover + or on_click. Any value other than "on_hover" or "on_click" will + be treated as None. Defaults to "on_hover". + zoom_to_layer (bool, optional): Whether to zoom to the layer. Defaults to False. + encoding (str, optional): The encoding of the GeoDataFrame. Defaults to "utf-8". + **kwargs: Additional keyword arguments to pass to add_gdf. + + Raises: + ImportError: If polars or required dependencies are not installed. + ValueError: If the specified geometry column is not found or contains invalid data. + TypeError: If the input is not a Polars DataFrame. + + Examples: + >>> import polars as pl + >>> # With Polars-ST + >>> df = pl.read_parquet("data.geoparquet") + >>> m = leafmap.Map() + >>> m.add_polars(df, geometry="geometry", crs="EPSG:4326") + """ + import warnings + + try: + import polars as pl + except ImportError: + raise ImportError( + "polars is required for add_polars(). " + "Install it with: pip install polars" + ) + + try: + import geopandas as gpd + from shapely import wkb, wkt + except ImportError: + raise ImportError( + "geopandas and shapely are required. " + "Install them with: pip install geopandas shapely" + ) + + # Validate input + if not isinstance(df, pl.DataFrame): + raise TypeError( + f"Expected a Polars DataFrame, got {type(df).__name__}. " + "Use add_gdf() for GeoPandas DataFrames." + ) + + # Check if geometry column exists + if geometry not in df.columns: + raise ValueError( + f"Geometry column '{geometry}' not found. " + f"Available columns: {df.columns}" + ) + + # Check for null/empty geometry column + if df[geometry].null_count() == len(df): + raise ValueError( + f"Geometry column '{geometry}' contains only null values. " + "Please provide valid geometry data." + ) + + # Convert Polars DataFrame to pandas + pdf = df.to_pandas() + + # Handle datetime columns (same as add_gdf) + for col in pdf.columns: + try: + if pdf[col].dtype in ["datetime64[ns]", "datetime64[ns, UTC]"]: + pdf[col] = pdf[col].astype(str) + except Exception: + pass + + # Handle geometry column - could be WKB binary or WKT string + geom_col = pdf[geometry] + + # Try to convert geometries using vectorized operations where possible + geometries = None + parse_error = None + + # Try WKB first (most common for Polars-ST) + if pdf[geometry].dtype == object: + # Check if first non-null value is bytes + first_valid = ( + geom_col.dropna().iloc[0] if len(geom_col.dropna()) > 0 else None + ) + + if first_valid is not None and isinstance(first_valid, bytes): + # Use vectorized from_wkb for better performance + try: + geometries = gpd.GeoSeries.from_wkb(geom_col) + except (TypeError, ValueError) as e: + parse_error = f"WKB parsing failed: {e}" + else: + # Try WKT string parsing + try: + geometries = gpd.GeoSeries.from_wkt(geom_col) + except (TypeError, ValueError) as e: + # Last resort: assume it's already shapely geometries + try: + geometries = gpd.GeoSeries(geom_col) + except (TypeError, ValueError) as e2: + parse_error = f"WKT parsing failed: {e}, Shapely geometry: {e2}" + else: + # Might be serialized as bytes dtype + try: + geometries = gpd.GeoSeries.from_wkb(geom_col) + except (TypeError, ValueError) as e: + parse_error = f"WKB parsing failed for bytes dtype: {e}" + + if geometries is None or parse_error: + raise ValueError( + f"Failed to parse geometry column '{geometry}'. " + f"Expected WKB binary or WKT string format. {parse_error or ''}" + ) + + # Drop the original geometry column and create GeoDataFrame + pdf = pdf.drop(columns=[geometry]) + gdf = gpd.GeoDataFrame(pdf, geometry=geometries) + + # Set CRS + if crs is not None: + gdf.crs = crs + elif gdf.crs is None: + # Default to EPSG:4326 with warning + warnings.warn( + f"No CRS specified for geometry column '{geometry}'. " + "Defaulting to EPSG:4326. Use the 'crs' parameter to specify a different CRS.", + UserWarning, + stacklevel=2, + ) + gdf.crs = "EPSG:4326" + + # Now use the existing add_gdf method + self.add_gdf( + gdf, + layer_name=layer_name, + style=style, + hover_style=hover_style, + style_callback=style_callback, + fill_colors=fill_colors, + info_mode=info_mode, + zoom_to_layer=zoom_to_layer, + encoding=encoding, + **kwargs, + ) + def add_gdf_time_slider( self, gdf: "gpd.GeoDataFrame", diff --git a/leafmap/maplibregl.py b/leafmap/maplibregl.py index 8e6141a171..7addf8c873 100644 --- a/leafmap/maplibregl.py +++ b/leafmap/maplibregl.py @@ -1809,6 +1809,173 @@ def add_tile_layer( self.set_visibility(name, visible) self.set_opacity(name, opacity) + def add_polars( + self, + df, + geometry: Optional[str] = "geometry", + crs: Optional[str] = None, + layer_type: Optional[str] = None, + filter: Optional[Dict] = None, + paint: Optional[Dict] = None, + name: Optional[str] = None, + fit_bounds: bool = True, + visible: bool = True, + before_id: Optional[str] = None, + source_args: Dict = {}, + overwrite: bool = False, + **kwargs: Any, + ): + """ + Adds a Polars DataFrame with geometry to the map. + + This method supports Polars-ST DataFrames with spatial geometry columns + and enables direct visualization of Polars-based geospatial data without + manual conversion to GeoPandas. + + Args: + df: A Polars DataFrame with a geometry column (e.g., from Polars-ST). + geometry (str, optional): The name of the geometry column. Defaults to "geometry". + crs (str, optional): The CRS of the geometry data (e.g., "EPSG:4326"). + If None, will try to detect from Polars-ST metadata or default to EPSG:4326. + layer_type (str, optional): The type of the layer. If None, the type + is inferred from the GeoJSON data. + filter (dict, optional): The filter to apply to the layer. If None, + no filter is applied. + paint (dict, optional): The paint properties to apply to the layer. + If None, no paint properties are applied. + name (str, optional): The name of the layer. If None, a random name + is generated. + fit_bounds (bool, optional): Whether to adjust the viewport of the + map to fit the bounds of the GeoJSON data. Defaults to True. + visible (bool, optional): Whether the layer is visible or not. + Defaults to True. + before_id (str, optional): The ID of an existing layer before which + the new layer should be inserted. + source_args (dict, optional): Additional keyword arguments that are + passed to the GeoJSONSource class. + overwrite (bool, optional): Whether to overwrite an existing layer with the same name. + Defaults to False. + **kwargs: Additional keyword arguments that are passed to the Layer class. + + Raises: + ImportError: If polars or required dependencies are not installed. + ValueError: If the specified geometry column is not found. + TypeError: If the input is not a Polars DataFrame. + + Examples: + >>> import polars as pl + >>> # With Polars-ST + >>> df = pl.read_parquet("data.geoparquet") + >>> m = leafmap.Map() + >>> m.add_polars(df, geometry="geometry", crs="EPSG:4326") + """ + try: + import polars as pl + except ImportError: + raise ImportError( + "polars is required for add_polars(). " + "Install it with: pip install polars" + ) + + try: + import geopandas as gpd + from shapely import wkb + except ImportError: + raise ImportError( + "geopandas and shapely are required. " + "Install them with: pip install geopandas shapely" + ) + + # Validate input + if not isinstance(df, pl.DataFrame): + raise TypeError( + f"Expected a Polars DataFrame, got {type(df).__name__}. " + "Use add_gdf() for GeoPandas DataFrames." + ) + + # Check if geometry column exists + if geometry not in df.columns: + raise ValueError( + f"Geometry column '{geometry}' not found. " + f"Available columns: {df.columns}" + ) + + # Convert Polars DataFrame to GeoPandas + # Strategy: Convert to pandas first, then create GeoDataFrame + pdf = df.to_pandas() + + # Handle geometry column - could be WKB binary or WKT string + geom_col = pdf[geometry] + + # Try to convert geometries + try: + # Check if it's binary (WKB from Polars-ST) + if pdf[geometry].dtype == object: + # Try WKB first (most common for Polars-ST) + try: + geometries = geom_col.apply( + lambda x: wkb.loads(bytes(x)) if x is not None else None + ) + except Exception: + # Try WKT + try: + from shapely import wkt + + geometries = geom_col.apply( + lambda x: wkt.loads(str(x)) if x is not None else None + ) + except Exception: + # Assume it's already shapely geometry + geometries = geom_col + else: + # Might be serialized as bytes + geometries = geom_col.apply( + lambda x: wkb.loads(x) if x is not None else None + ) + except Exception as e: + raise ValueError( + f"Failed to parse geometry column '{geometry}'. " + f"Expected WKB binary or WKT string format. Error: {e}" + ) + + # Drop the original geometry column and create GeoDataFrame + pdf = pdf.drop(columns=[geometry]) + gdf = gpd.GeoDataFrame(pdf, geometry=geometries) + + # Set CRS + if crs is not None: + gdf.crs = crs + elif gdf.crs is None: + # Try to detect CRS from Polars-ST metadata + # Polars-ST stores CRS in column metadata + try: + # Check if the original Polars column has metadata + if ( + hasattr(df[geometry], "_metadata") + and "crs" in df[geometry]._metadata + ): + gdf.crs = df[geometry]._metadata["crs"] + else: + # Default to EPSG:4326 + gdf.crs = "EPSG:4326" + except Exception: + gdf.crs = "EPSG:4326" + + # Now use the existing add_gdf method + self.add_gdf( + gdf, + layer_type=layer_type, + filter=filter, + paint=paint, + name=name, + fit_bounds=fit_bounds, + visible=visible, + before_id=before_id, + source_args=source_args, + overwrite=overwrite, + **kwargs, + ) + def add_vector_tile( self, url: str, diff --git a/mkdocs.yml b/mkdocs.yml index 4dc1a9005b..93def699f3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -408,3 +408,4 @@ nav: - notebooks/107_copernicus.ipynb - notebooks/108_add_geotiff.ipynb - notebooks/109_local_titiler.ipynb + - notebooks/110_polars.ipynb diff --git a/pyproject.toml b/pyproject.toml index d6df8a464c..b3f5ea9227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ maplibre = ["anywidget", "geopandas", "fiona", "h3", "ipyvuetify", "localtileser gdal = ["gdal", "pyproj"] duckdb = ["duckdb", "duckdb-engine", "jupysql", "flask", "flask-cors", "jupyter-server-proxy"] titiler = ["titiler", "uvicorn"] +polars = ["polars", "geopandas", "shapely"] [project.scripts]