From 1d201cea9c604824d7b91a6535120a527b4f81c8 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Mon, 5 Aug 2024 16:50:50 -0400 Subject: [PATCH] Improve csv_to_vector and lonboard module (#864) * Improve csv_to_vector and lonboard module * Add docstrings --- leafmap/common.py | 34 ++++++-- leafmap/deckgl.py | 201 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 196 insertions(+), 39 deletions(-) diff --git a/leafmap/common.py b/leafmap/common.py index aaca541acf..048482bda1 100644 --- a/leafmap/common.py +++ b/leafmap/common.py @@ -900,13 +900,23 @@ def csv_to_geojson( f.write(json.dumps(geojson)) -def csv_to_gdf(in_csv, latitude="latitude", longitude="longitude", encoding="utf-8"): +def csv_to_gdf( + in_csv, + latitude="latitude", + longitude="longitude", + geometry=None, + crs="EPSG:4326", + encoding="utf-8", + **kwargs, +): """Creates points for a CSV file and converts them to a GeoDataFrame. Args: in_csv (str): The file path to the input CSV file. latitude (str, optional): The name of the column containing latitude coordinates. Defaults to "latitude". longitude (str, optional): The name of the column containing longitude coordinates. Defaults to "longitude". + geometry (str, optional): The name of the column containing geometry. Defaults to None. + crs (str, optional): The coordinate reference system. Defaults to "EPSG:4326". encoding (str, optional): The encoding of characters. Defaults to "utf-8". Returns: @@ -916,14 +926,21 @@ def csv_to_gdf(in_csv, latitude="latitude", longitude="longitude", encoding="utf check_package(name="geopandas", URL="https://geopandas.org") import geopandas as gpd + import pandas as pd + from shapely import wkt out_dir = os.getcwd() - out_geojson = os.path.join(out_dir, random_string() + ".geojson") - csv_to_geojson(in_csv, out_geojson, latitude, longitude, encoding) + if geometry is None: + out_geojson = os.path.join(out_dir, random_string() + ".geojson") + csv_to_geojson(in_csv, out_geojson, latitude, longitude, encoding=encoding) - gdf = gpd.read_file(out_geojson) - os.remove(out_geojson) + gdf = gpd.read_file(out_geojson) + os.remove(out_geojson) + else: + df = pd.read_csv(in_csv, encoding=encoding) + df["geometry"] = df[geometry].apply(wkt.loads) + gdf = gpd.GeoDataFrame(df, geometry="geometry", crs=crs, **kwargs) return gdf @@ -932,6 +949,8 @@ def csv_to_vector( output, latitude="latitude", longitude="longitude", + geometry=None, + crs="EPSG:4326", encoding="utf-8", **kwargs, ): @@ -942,10 +961,13 @@ def csv_to_vector( output (str): The file path to the output vector dataset. latitude (str, optional): The name of the column containing latitude coordinates. Defaults to "latitude". longitude (str, optional): The name of the column containing longitude coordinates. Defaults to "longitude". + geometry (str, optional): The name of the column containing geometry. Defaults to None. + crs (str, optional): The coordinate reference system. Defaults to "EPSG:4326". encoding (str, optional): The encoding of characters. Defaults to "utf-8". + **kwargs: Additional keyword arguments to pass to gdf.to_file(). """ - gdf = csv_to_gdf(in_csv, latitude, longitude, encoding) + gdf = csv_to_gdf(in_csv, latitude, longitude, geometry, crs, encoding) gdf.to_file(output, **kwargs) diff --git a/leafmap/deckgl.py b/leafmap/deckgl.py index 0c168bc4e2..4557bbc572 100644 --- a/leafmap/deckgl.py +++ b/leafmap/deckgl.py @@ -1,4 +1,7 @@ +from box import Box + from typing import Union, List, Dict, Optional, Tuple, Any +from .basemaps import xyz_to_leaflet from .common import * from .map_widgets import * from .plot import * @@ -12,6 +15,8 @@ "lonboard needs to be installed to use this module. Use 'pip install lonboard' to install the package." ) +basemaps = Box(xyz_to_leaflet(), frozen_box=True) + class Map(lonboard.Map): """The Map class inherits lonboard.Map. @@ -67,6 +72,8 @@ def add_gdf( color_map: Optional[Union[str, Dict]] = None, color_k: Optional[int] = 5, color_args: dict = {}, + alpha: Optional[float] = 1.0, + rescale: bool = True, zoom: Optional[float] = 10.0, **kwargs: Any, ) -> None: @@ -97,6 +104,7 @@ def add_gdf( """ from lonboard import ScatterplotLayer, PathLayer, SolidPolygonLayer + import matplotlib.pyplot as plt geom_type = gdf.geometry.iloc[0].geom_type kwargs["pickable"] = pickable @@ -106,18 +114,14 @@ def add_gdf( kwargs["get_radius"] = 10 if color_column is not None: if isinstance(color_map, str): - kwargs["get_fill_color"] = assign_continuous_colors( - gdf, - color_column, - color_map, - scheme=color_scheme, - k=color_k, - **color_args, + kwargs["get_fill_color"] = apply_continuous_cmap( + gdf[color_column], color_map, alpha, rescale ) elif isinstance(color_map, dict): - kwargs["get_fill_color"] = assign_discrete_colors( - gdf, color_column, color_map, to_rgb=True, return_type="array" + kwargs["get_fill_color"] = apply_categorical_cmap( + gdf[color_column], color_map, alpha ) + if "get_fill_color" not in kwargs: kwargs["get_fill_color"] = [255, 0, 0, 180] layer = ScatterplotLayer.from_geopandas(gdf, **kwargs) @@ -126,33 +130,24 @@ def add_gdf( kwargs["get_width"] = 5 if color_column is not None: if isinstance(color_map, str): - kwargs["get_color"] = assign_continuous_colors( - gdf, - color_column, - color_map, - scheme=color_scheme, - k=color_k, - **color_args, + cmap = plt.get_cmap(color_map) + kwargs["get_color"] = apply_continuous_cmap( + gdf[color_column], cmap, alpha, rescale ) elif isinstance(color_map, dict): - kwargs["get_color"] = assign_discrete_colors( - gdf, color_column, color_map, to_rgb=True, return_type="array" + kwargs["get_color"] = apply_categorical_cmap( + gdf[color_column], color_map, alpha ) layer = PathLayer.from_geopandas(gdf, **kwargs) elif geom_type in ["Polygon", "MultiPolygon"]: if color_column is not None: if isinstance(color_map, str): - kwargs["get_fill_color"] = assign_continuous_colors( - gdf, - color_column, - color_map, - scheme=color_scheme, - k=color_k, - **color_args, + kwargs["get_fill_color"] = apply_continuous_cmap( + gdf[color_column], color_map, alpha, rescale ) elif isinstance(color_map, dict): - kwargs["get_fill_color"] = assign_discrete_colors( - gdf, color_column, color_map, to_rgb=True, return_type="array" + kwargs["get_fill_color"] = apply_categorical_cmap( + gdf[color_column], color_map, alpha ) if "get_fill_color" not in kwargs: kwargs["get_fill_color"] = [0, 0, 255, 128] @@ -254,18 +249,37 @@ def add_layer( None """ - from lonboard import ScatterplotLayer, PathLayer, SolidPolygonLayer + from lonboard import ( + BitmapLayer, + BitmapTileLayer, + HeatmapLayer, + PathLayer, + PointCloudLayer, + PolygonLayer, + ScatterplotLayer, + SolidPolygonLayer, + ) - if type(layer) in [ScatterplotLayer, PathLayer, SolidPolygonLayer]: + if type(layer) in [ + BitmapLayer, + BitmapTileLayer, + HeatmapLayer, + ScatterplotLayer, + PathLayer, + PointCloudLayer, + PolygonLayer, + SolidPolygonLayer, + ]: self.layers = self.layers + [layer] if zoom_to_layer: from lonboard._viewport import compute_view - try: - self.view_state = compute_view([self.layers[-1].table]) - except Exception as e: - print(e) + if hasattr(layer, "table"): + try: + self.view_state = compute_view([self.layers[-1].table]) + except Exception as e: + print(e) else: self.add_vector( layer, zoom_to_layer=zoom_to_layer, pickable=pickable, **kwargs @@ -317,3 +331,124 @@ def to_streamlit( except Exception as e: raise e + + def add_basemap(self, basemap="HYBRID", visible=True, **kwargs) -> None: + """Adds a basemap to the map. + + Args: + basemap (str, optional): Can be one of string from basemaps. Defaults to 'HYBRID'. + visible (bool, optional): Whether the basemap is visible or not. Defaults to True. + **kwargs: Keyword arguments for the TileLayer. + """ + import xyzservices + + try: + + map_dict = { + "ROADMAP": "Google Maps", + "SATELLITE": "Google Satellite", + "TERRAIN": "Google Terrain", + "HYBRID": "Google Hybrid", + } + + if isinstance(basemap, str): + if basemap.upper() in map_dict: + tile = get_google_map(basemap.upper()) + + layer = lonboard.BitmapTileLayer( + data=tile.url, + min_zoom=tile.min_zoom, + max_zoom=tile.max_zoom, + visible=visible, + **kwargs, + ) + + self.add_layer(layer) + return + + if isinstance(basemap, xyzservices.TileProvider): + url = basemap.build_url() + if "max_zoom" in basemap.keys(): + max_zoom = basemap["max_zoom"] + else: + max_zoom = 22 + layer = lonboard.BitmapTileLayer( + data=url, + min_zoom=tile.min_zoom, + max_zoom=max_zoom, + visible=visible, + **kwargs, + ) + + self.add_layer(layer) + elif basemap in basemaps and basemaps[basemap].name: + tile = basemaps[basemap] + layer = lonboard.BitmapTileLayer( + data=tile.url, + min_zoom=tile.get("min_zoom", 0), + max_zoom=tile.get("max_zoom", 24), + visible=visible, + **kwargs, + ) + self.add_layer(layer) + else: + print( + "Basemap can only be one of the following:\n {}".format( + "\n ".join(basemaps.keys()) + ) + ) + + except Exception as e: + raise ValueError( + "Basemap can only be one of the following:\n {}".format( + "\n ".join(basemaps.keys()) + ) + ) + + +def apply_continuous_cmap(values, cmap, alpha=None, rescale=True, **kwargs): + """ + Apply a continuous colormap to a set of values. + + This function rescales the input values to the range [0, 1] if `rescale` is True, + and then applies the specified colormap. + + Args: + values (array-like): The input values to which the colormap will be applied. + cmap (str or Colormap): The colormap to apply. Can be a string name of a matplotlib colormap or a Colormap object. + alpha (float, optional): The alpha transparency to apply to the colormap. Defaults to None. + rescale (bool, optional): If True, rescales the input values to the range [0, 1]. Defaults to True. + **kwargs: Additional keyword arguments to pass to the colormap function. + + Returns: + array: The colors mapped to the input values. + """ + import numpy as np + import matplotlib.pyplot as plt + + if rescale: + values = np.array(values) + values = (values - values.min()) / (values.max() - values.min()) + + if isinstance(cmap, str): + cmap = plt.get_cmap(cmap) + + return lonboard.colormap.apply_continuous_cmap(values, cmap, alpha=alpha, **kwargs) + + +def apply_categorical_cmap(values, cmap, alpha=None, **kwargs): + """ + Apply a categorical colormap to a set of values. + + This function applies a specified categorical colormap to the input values. + + Args: + values (array-like): The input values to which the colormap will be applied. + cmap (str or Colormap): The colormap to apply. Can be a string name of a matplotlib colormap or a Colormap object. + alpha (float, optional): The alpha transparency to apply to the colormap. Defaults to None. + **kwargs: Additional keyword arguments to pass to the colormap function. + + Returns: + array: The colors mapped to the input values. + """ + return lonboard.colormap.apply_categorical_cmap(values, cmap, alpha=alpha, **kwargs)