From 39b9c919bd5de6baba52ba9fbac6edd95afd6ae5 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Thu, 22 Aug 2024 16:11:52 -0400 Subject: [PATCH] Add support for DeckGL ArcLayer (#877) --- leafmap/common.py | 40 ++++++++++++++++++ leafmap/maplibregl.py | 97 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/leafmap/common.py b/leafmap/common.py index 0a466f96e8..d208dab582 100644 --- a/leafmap/common.py +++ b/leafmap/common.py @@ -14297,3 +14297,43 @@ def create_polyline(row): gdf = gdf.set_geometry("geometry") gdf.crs = crs return gdf + + +def read_file(data: str, **kwargs: Any) -> Union[pd.DataFrame, "gpd.GeoDataFrame"]: + """ + Reads a file and returns a DataFrame or GeoDataFrame. + + Args: + data (str): The file path or a DataFrame/GeoDataFrame. + **kwargs (Any): Additional arguments passed to the file reading function. + + Returns: + Union[pd.DataFrame, gpd.GeoDataFrame]: The read data as a DataFrame or GeoDataFrame. + + Raises: + ValueError: If the data type is unsupported. + """ + import geopandas as gpd + + if isinstance(data, str): + if data.endswith(".parquet"): + df = pd.read_parquet(data, **kwargs) + elif data.endswith(".csv"): + df = pd.read_csv(data, **kwargs) + elif data.endswith(".json"): + df = pd.read_json(data, **kwargs) + elif data.endswith(".xlsx"): + df = pd.read_excel(data, **kwargs) + else: + df = gpd.read_file(data, **kwargs) + elif isinstance(data, dict) or isinstance(data, list): + df = pd.DataFrame(data, **kwargs) + + elif isinstance(data, pd.DataFrame) or isinstance(data, gpd.GeoDataFrame): + df = data + else: + raise ValueError( + "Unsupported data type. Please provide a file path or a DataFrame." + ) + + return df diff --git a/leafmap/maplibregl.py b/leafmap/maplibregl.py index 12d978fdf4..b9938e854a 100644 --- a/leafmap/maplibregl.py +++ b/leafmap/maplibregl.py @@ -226,6 +226,103 @@ def remove_layer(self, name: str) -> None: if name in self.layer_dict: self.layer_dict.pop(name) + def add_deck_layers(self, layers: list[dict], tooltip: str | dict = None) -> None: + """Add Deck.GL layers to the layer stack + + Args: + layers (list[dict]): A list of dictionaries containing the Deck.GL layers to be added. + tooltip (str | dict): Either a single mustache template string applied to all layers + or a dictionary where keys are layer ids and values are mustache template strings. + """ + super().add_deck_layers(layers, tooltip) + + for layer in layers: + + self.layer_dict[layer["id"]] = { + "layer": layer, + "opacity": layer.get("opacity", 1.0), + "visible": layer.get("visible", True), + "type": layer.get("@@type", "deck"), + "color": layer.get("getFillColor", "#ffffff"), + } + + def add_arc_layer( + self, + data: Union[str, pd.DataFrame], + src_lon: str, + src_lat: str, + dst_lon: str, + dst_lat: str, + src_color: List[int] = [255, 0, 0], + dst_color: List[int] = [255, 255, 0], + line_width: int = 2, + layer_id: str = "arc_layer", + pickable: bool = True, + tooltip: Optional[Union[str, List[str]]] = None, + **kwargs: Any, + ) -> None: + """ + Add a DeckGL ArcLayer to the map. + + Args: + data (Union[str, pd.DataFrame]): The file path or DataFrame containing the data. + src_lon (str): The source longitude column name. + src_lat (str): The source latitude column name. + dst_lon (str): The destination longitude column name. + dst_lat (str): The destination latitude column name. + src_color (List[int]): The source color as an RGB list. + dst_color (List[int]): The destination color as an RGB list. + line_width (int): The width of the lines. + layer_id (str): The ID of the layer. + pickable (bool): Whether the layer is pickable. + tooltip (Optional[Union[str, List[str]]], optional): The tooltip content or list of columns. Defaults to None. + **kwargs (Any): Additional arguments for the layer. + + Returns: + None + """ + + df = read_file(data) + if "geometry" in df.columns: + df = df.drop(columns=["geometry"]) + + arc_data = [ + { + "source_position": [row[src_lon], row[src_lat]], + "target_position": [row[dst_lon], row[dst_lat]], + **row.to_dict(), # Include other columns + } + for _, row in df.iterrows() + ] + + # Generate tooltip template dynamically based on the columns + if tooltip is None: + columns = df.columns + elif isinstance(tooltip, list): + columns = tooltip + tooltip_content = "
".join([f"{col}: {{{{ {col} }}}}" for col in columns]) + + deck_arc_layer = { + "@@type": "ArcLayer", + "id": layer_id, + "data": arc_data, + "getSourcePosition": "@@=source_position", + "getTargetPosition": "@@=target_position", + "getSourceColor": src_color, + "getTargetColor": dst_color, + "getWidth": line_width, + "pickable": pickable, + } + + deck_arc_layer.update(kwargs) + + self.add_deck_layers( + [deck_arc_layer], + tooltip={ + layer_id: tooltip_content, + }, + ) + def add_control( self, control: Union[str, Any], position: str = "top-right", **kwargs: Any ) -> None: