Skip to content

Commit

Permalink
Add add_gps_trace method for MapLibre (#969)
Browse files Browse the repository at this point in the history
* Add add_gps_trace method for MapLibre

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
giswqs and pre-commit-ci[bot] authored Nov 10, 2024
1 parent 59c100e commit 8157f6d
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 0 deletions.
92 changes: 92 additions & 0 deletions docs/maplibre/gps_trace.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"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=maplibre/gps_trace.ipynb)\n",
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/gps_trace.ipynb)\n",
"[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n",
"\n",
"**Visualizing GPS Trace Data**\n",
"\n",
"Uncomment the following line to install [leafmap](https://leafmap.org) if needed."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"# %pip install \"leafmap[maplibre]\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2",
"metadata": {},
"outputs": [],
"source": [
"import leafmap.maplibregl as leafmap"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"data = (\n",
" \"https://github.com/opengeos/datasets/releases/download/vector/hike_gps_trace.csv\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": [
"m = leafmap.Map(style=\"3d-hybrid\")\n",
"m.add_gps_trace(data, radius=5, add_line=True)\n",
"m.add_layer_control()\n",
"m"
]
},
{
"cell_type": "markdown",
"id": "5",
"metadata": {},
"source": [
"![image](https://github.com/user-attachments/assets/2a669b4d-c43f-4382-bf66-8918b4719b48)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
6 changes: 6 additions & 0 deletions docs/maplibre/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ Add Google Earth Engine data layers to a map.

[![](https://i.imgur.com/oHQDf79.png)](https://leafmap.org/maplibre/google_earth_engine)

## Visualize GPS Trace Data

Add a GPS trace to a map.

[![](https://github.com/user-attachments/assets/2a669b4d-c43f-4382-bf66-8918b4719b48)](https://leafmap.org/maplibre/gps_trace)

## Create a heatmap layer

Visualize earthquake frequency by location using a heatmap layer.
Expand Down
74 changes: 74 additions & 0 deletions leafmap/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15018,3 +15018,77 @@ def download_nlcd(
basename = os.path.basename(year_url)
filepath = os.path.join(out_dir, basename)
download_file(year_url, filepath, quiet=quiet, **kwargs)


def connect_points_as_line(
gdf: "GeoDataFrame", sort_column: Optional[str] = None, crs: str = "EPSG:4326"
) -> "GeoDataFrame":
"""
Connects points in a GeoDataFrame into a single LineString based on a specified sort column
or the index if no column is provided. The resulting GeoDataFrame will have the specified CRS.
Args:
gdf (GeoDataFrame): A GeoDataFrame containing point geometries.
sort_column (Optional[str]): Column name to sort the points by (e.g., 'timestamp').
If None, the index is used for sorting. Defaults to None.
crs (str): The coordinate reference system (CRS) for the resulting GeoDataFrame.
Defaults to "EPSG:4326".
Returns:
GeoDataFrame: A new GeoDataFrame containing a single LineString geometry that connects
all points in the specified order, with the specified CRS.
Example:
>>> line_gdf = connect_points_as_line(gdf, 'timestamp', crs="EPSG:3857")
>>> line_gdf = connect_points_as_line(gdf) # Uses index and defaults to EPSG:4326
"""
from shapely.geometry import LineString
import geopandas as gpd

# Sort the GeoDataFrame by the specified column or by index if None
gdf_sorted = gdf.sort_values(by=sort_column) if sort_column else gdf.sort_index()

# Extract the point geometries and create a LineString
line = LineString(gdf_sorted.geometry.tolist())

# Create a new GeoDataFrame with the LineString and the specified CRS
line_gdf = gpd.GeoDataFrame(geometry=[line], crs=crs)

return line_gdf


def line_to_points(data: str) -> "GeoDataFrame":
"""
Converts a LineString geometry in a GeoDataFrame into individual points.
Args:
line_gdf (GeoDataFrame): A GeoDataFrame containing a LineString geometry.
Returns:
GeoDataFrame: A new GeoDataFrame where each vertex of the LineString is a Point geometry.
"""
import geopandas as gpd
from shapely.geometry import Point, LineString
from geopandas import GeoDataFrame

if isinstance(data, str):
line_gdf = gpd.read_file(data)
elif isinstance(data, GeoDataFrame):
line_gdf = data
else:
raise ValueError("Invalid input. Must be a file path or a GeoDataFrame.")

# Ensure there is a LineString in the GeoDataFrame
if not all(line_gdf.geometry.type == "LineString"):
raise ValueError("Input GeoDataFrame must contain only LineString geometries.")

# Extract the first (and only) LineString from the GeoDataFrame
line = line_gdf.geometry.iloc[0]

# Convert each point in the LineString to a Point geometry
points = [Point(coord) for coord in line.coords]

# Create a new GeoDataFrame with these points
points_gdf = gpd.GeoDataFrame(geometry=points, crs=line_gdf.crs)

return points_gdf
109 changes: 109 additions & 0 deletions leafmap/maplibregl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3401,6 +3401,115 @@ def add_nlcd(self, years: list = [2023], add_legend: bool = True, **kwargs) -> N
if add_legend:
self.add_legend(title="NLCD Land Cover Type", builtin_legend="NLCD")

def add_gps_trace(
self,
data: Union[str, List[Dict[str, Any]]],
x: str = "longitude",
y: str = "latitude",
columns: Optional[List[str]] = None,
colormap: Optional[Dict[str, str]] = None,
radius: int = 5,
circle_color: Optional[Union[str, List[Any]]] = None,
stroke_color: str = "#ffffff",
opacity: float = 1.0,
paint: Optional[Dict[str, Any]] = None,
name: str = "GPS Trace",
add_line: bool = False,
sort_column: Optional[str] = None,
line_args: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
"""
Adds a GPS trace to the map.
Args:
data (Union[str, List[Dict[str, Any]]]): The GPS trace data. It can be a GeoJSON file path or a list of coordinates.
x (str, optional): The column name for the x coordinates. Defaults to "longitude".
y (str, optional): The column name for the y coordinates. Defaults to "latitude".
columns (Optional[List[str]], optional): The list of columns to include in the GeoDataFrame. Defaults to None.
colormap (Optional[Dict[str, str]], optional): The colormap for the GPS trace. Defaults to None.
radius (int, optional): The radius of the GPS trace points. Defaults to 5.
circle_color (Optional[Union[str, List[Any]]], optional): The color of the GPS trace points. Defaults to None.
stroke_color (str, optional): The stroke color of the GPS trace points. Defaults to "#ffffff".
opacity (float, optional): The opacity of the GPS trace points. Defaults to 1.0.
paint (Optional[Dict[str, Any]], optional): The paint properties for the GPS trace points. Defaults to None.
name (str, optional): The name of the GPS trace layer. Defaults to "GPS Trace".
add_line (bool, optional): If True, adds a line connecting the GPS trace points. Defaults to False.
sort_column (Optional[str], optional): The column name to sort the points before connecting them as a line. Defaults to None.
line_args (Optional[Dict[str, Any]], optional): Additional arguments for the line layer. Defaults to None.
**kwargs (Any): Additional keyword arguments to pass to the add_geojson method.
Returns:
None
"""
import geopandas as gpd

if isinstance(data, str):
gdf = points_from_xy(data, x=x, y=y)
elif isinstance(data, gpd.GeoDataFrame):
gdf = data
else:
raise ValueError(
"Invalid data type. Use a GeoDataFrame or a list of coordinates."
)

setattr(self, "gps_trace", gdf)

if add_line:
line_gdf = connect_points_as_line(gdf, sort_column=sort_column)
else:
line_gdf = None

if colormap is None:
colormap = {
"doorstep": "#FF0000", # Red
"indoor": "#0000FF", # Blue
"outdoor": "#00FF00", # Green
"parked": "#000000", # Yellow
"selected": "#FFFF00",
}

if columns is None:
if "annotation" in gdf.columns:
columns = ["latitude", "longitude", "annotation", "geometry"]
gdf = gdf[columns]
if circle_color is None:
circle_color = [
"match",
["get", "annotation"],
"doorstep",
colormap["doorstep"],
"indoor",
colormap["indoor"],
"outdoor",
colormap["outdoor"],
"parked",
colormap["parked"],
"selected",
colormap["selected"],
"#CCCCCC", # Default color if annotation does not match
]

if circle_color is None:
circle_color = "#3388ff"

geojson = gdf.__geo_interface__

if paint is None:
paint = {
"circle-radius": radius,
"circle-color": circle_color,
"circle-stroke-color": stroke_color,
"circle-stroke-width": 1,
"circle-opacity": opacity,
}

if line_gdf is not None:
if line_args is None:
line_args = {}
self.add_gdf(line_gdf, name="GPS Trace Line", **line_args)
self.add_geojson(geojson, layer_type="circle", paint=paint, name=name, **kwargs)


class Container(v.Container):

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ nav:
- maplibre/geojson_polygon.ipynb
- maplibre/geopandas.ipynb
- maplibre/google_earth_engine.ipynb
- maplibre/gps_trace.ipynb
- maplibre/heatmap_layer.ipynb
- maplibre/interactive_false.ipynb
- maplibre/jump_to.ipynb
Expand Down

0 comments on commit 8157f6d

Please sign in to comment.