paint-brush
我如何使用 Python 和 Folium 可视化我的户外活动经过@lukaskrimphove
7,707 讀數
7,707 讀數

我如何使用 Python 和 Folium 可视化我的户外活动

经过 Lukas Krimphove14m2023/09/06
Read on Terminal Reader

太長; 讀書

踏上探索和测绘之旅!了解如何为 GPX 文件注入活力并使用 Folium 创建交互式地图。
featured image - 我如何使用 Python 和 Folium 可视化我的户外活动
Lukas Krimphove HackerNoon profile picture
0-item

我梦想从慕尼黑徒步到威尼斯,徒步穿越美丽的阿尔卑斯山。但由于我仍然要过日常生活,所以我的旅程必须由多个阶段组成,每次冒险之间有几周、几个月甚至几年的时间。没关系,因为它更多的是旅程而不是目的地。然而,我一直想要一种方法来直观地追溯这些路径,看看我已经走了多远,距离我的目标有多近。我想要一种方式来庆祝我所取得的进步并激励自己走得更远。


幸运的是,许多户外和运动应用程序,如 Adidas Running、Komoot、Strava 等,都慷慨地允许我们将活动导出为 GPX 文件。然而,这些 GPX 文件无处可去。


这就是 Python 和Folium发挥作用的地方。我最近偶然发现了 Folium,一个用于创建交互式地图的强大 Python 库。它可以轻松合并地理数据,例如 GPX 文件,并允许自定义和探索。利用我收集的大量 GPS 数据,我开始尝试使用 Folium 制作一张地图,让我的户外旅行变得生动起来。


经过一些研究和大量测试,我想出了一张地图,可以让我重新审视过去的户外活动:

因此,如果您像我一样拥有大量 GPS 数据,但没有任何目的,您可能会想知道我是如何到达那里的。因此,在本文中,我将解释如何为 GPX 文件注入活力。


让我们踏上探索和测绘之旅吧!

Jupyter 笔记本入门

为了开始这次冒险,我们将使用 Jupyter Notebook。为什么选择 Jupyter 笔记本?这是一个非常棒的交互式计算环境,允许我们将代码、可视化和文本结合起来,使其非常适合试验我们的数据和 Folium 库。


如果您尚未安装 Jupyter Notebook,请按照其官方网站上的说明进行操作。安装后,您可以创建一个新的 Jupyter Notebook 并为您的旅程做好准备。

解析 GPX 文件以获取轨迹数据

接下来,我们需要地图的原材料 - GPX 文件。 GPX(GPS 交换格式)是一种广泛使用的文件格式,用于存储位置数据,例如纬度、经度、海拔和时间,非常适合跟踪户外活动。


如果您是一位狂热的徒步旅行者、跑步者、骑自行车者或滑雪者,您很可能已经使用户外或运动应用程序跟踪过各种短途旅行。许多此类应用程序允许您以 GPX 格式导出您的活动。因此,收集这些 GPX 文件,让我们开始吧!


在我们的 Python 代码中,我们将使用 gpxpy 库来解析 GPX 文件并提取基本的轨迹数据,例如纬度、经度、海拔、速度和距离。 parse_gpx() 函数将为我们完成所有繁重的工作:


 ### READ GPX FILES import gpxpy import pandas as pd import numpy as np import haversine as hs from pathlib import Path def parse_gpx(file_path): # parse gpx file to pandas dataframe gpx = gpxpy.parse(open(file_path), version='1.0') data = [] points = [] for track in gpx.tracks: for segment in track.segments: for point_idx, point in enumerate(segment.points): points.append(tuple([point.latitude, point.longitude])) # calculate distances between points if point_idx == 0: distance = np.nan else: distance = hs.haversine( point1=points[point_idx-1], point2=points[point_idx], unit=hs.Unit.METERS ) data.append([point.longitude, point.latitude,point.elevation, point.time, segment.get_speed(point_idx), distance]) columns = ['Longitude', 'Latitude', 'Elevation', 'Time', 'Speed', 'Distance'] gpx_df = pd.DataFrame(data, columns=columns) return points, gpx_df activities = {} frames = [] for activity_type in ACTIVITY_TYPES: activities[activity_type] = {} pathlist = Path(activity_type).glob('**/*.gpx') for path in pathlist: # exclude hidden directories and files # will lead to performance issues if there are lots of hidden files if any(part.startswith('.') for part in path.parts): continue activity_group = path.parts[1] activity_name = path.parts[2] if activity_group not in activities[activity_type]: activities[activity_type][activity_group] = [] points, gpx_df = parse_gpx(path) gpx_df['Elevation_Diff'] = np.round(gpx_df['Elevation'].diff(), 2) gpx_df['Cum_Elevation'] = np.round(gpx_df['Elevation_Diff'].cumsum(), 2) gpx_df['Cum_Distance'] = np.round(gpx_df['Distance'].cumsum(), 2) gpx_df['Gradient'] = np.round(gpx_df['Elevation_Diff'] / gpx_df['Distance'] * 100, 1) activities[activity_type][activity_group].append({ 'name': activity_name.replace('.gpx', '').replace('_', ' '), 'points': points, 'gpx_df': gpx_df }) frames.append(gpx_df) df = pd.concat(frames) df

这给我们留下了活动的所有必要数据:所有 GPS 坐标的列表和包含各种指标的 Pandas DataFrame。

在交互式地图上绘制 GPX 轨迹

现在,我们的 GPS 数据被组织在 Pandas DataFrame 中,我们可以在交互式地图上可视化我们的户外活动。 Folium 使这项任务变得轻而易举:


 ### CONFIG # LOCATION = None LOCATION = [48.13743, 11.57549] # latitude, longitude ZOOM_START = 10 ACTIVITY_TYPES = { 'Hiking': { 'icon': 'person-hiking', 'color': 'green' }, 'Running': { 'icon': 'person-running', 'color': 'orange' }, 'Biking': { 'icon': 'person-biking', 'color': 'red' }, 'Skiing': { 'icon': 'person-skiing', 'color': 'blue' } }
  • 我们将创建一张以特定位置为中心的地图(您可以选择一个或让代码根据您的数据确定中心)。
  • 我们将为每种活动类型分配独特的颜色和图标,例如徒步旅行、跑步、骑自行车或滑雪。 ACTIVITY_TYPES 字典将帮助我们解决这个问题。
 ### READ GPX FILES import gpxpy import pandas as pd import numpy as np import haversine as hs from pathlib import Path def parse_gpx(file_path): # parse gpx file to pandas dataframe gpx = gpxpy.parse(open(file_path), version='1.0') data = [] points = [] for track in gpx.tracks: for segment in track.segments: for point_idx, point in enumerate(segment.points): points.append(tuple([point.latitude, point.longitude])) # calculate distances between points if point_idx == 0: distance = np.nan else: distance = hs.haversine( point1=points[point_idx-1], point2=points[point_idx], unit=hs.Unit.METERS ) data.append([point.longitude, point.latitude,point.elevation, point.time, segment.get_speed(point_idx), distance]) columns = ['Longitude', 'Latitude', 'Elevation', 'Time', 'Speed', 'Distance'] gpx_df = pd.DataFrame(data, columns=columns) return points, gpx_df activities = {} frames = [] for activity_type in ACTIVITY_TYPES: activities[activity_type] = {} pathlist = Path(activity_type).glob('**/*.gpx') for path in pathlist: # exclude hidden directories and files # will lead to performance issues if there are lots of hidden files if any(part.startswith('.') for part in path.parts): continue activity_group = path.parts[1] activity_name = path.parts[2] if activity_group not in activities[activity_type]: activities[activity_type][activity_group] = [] points, gpx_df = parse_gpx(path) gpx_df['Elevation_Diff'] = np.round(gpx_df['Elevation'].diff(), 2) gpx_df['Cum_Elevation'] = np.round(gpx_df['Elevation_Diff'].cumsum(), 2) gpx_df['Cum_Distance'] = np.round(gpx_df['Distance'].cumsum(), 2) gpx_df['Gradient'] = np.round(gpx_df['Elevation_Diff'] / gpx_df['Distance'] * 100, 1) activities[activity_type][activity_group].append({ 'name': activity_name.replace('.gpx', '').replace('_', ' '), 'points': points, 'gpx_df': gpx_df }) frames.append(gpx_df) df = pd.concat(frames) df That leaves us with all the necessary data of an activity: a list of all the GPS coordinates and a Pandas DataFrame containing all kinds of metrics. Plotting GPX Trails on the Interactive Map With our GPS data now organized in a Pandas DataFrame, we can visualize our outdoor activities on an interactive map. Folium makes this task a breeze: ### CONFIG # LOCATION = None LOCATION = [48.13743, 11.57549] # latitude, longitude ZOOM_START = 10 ACTIVITY_TYPES = { 'Hiking': { 'icon': 'person-hiking', 'color': 'green' }, 'Running': { 'icon': 'person-running', 'color': 'orange' }, 'Biking': { 'icon': 'person-biking', 'color': 'red' }, 'Skiing': { 'icon': 'person-skiing', 'color': 'blue' } } We'll create a map centered at a specific location (you can choose one or let the code determine the center based on your data). We'll assign unique colors and icons to each activity type, such as hiking, running, biking, or skiing. The ACTIVITY_TYPES dictionary will help us with this. ### CREATE MAP import folium from folium import plugins as folium_plugins if LOCATION: location = LOCATION else: location=[df.Latitude.mean(), df.Longitude.mean()] map = folium.Map(location=location, zoom_start=ZOOM_START, tiles=None) folium.TileLayer('OpenStreetMap', name='OpenStreet Map').add_to(map) folium.TileLayer('Stamen Terrain', name='Stamen Terrain').add_to(map) ### MAP TRAILS def timedelta_formatter(td): td_sec = td.seconds hour_count, rem = divmod(td_sec, 3600) hour_count += td.days * 24 minute_count, second_count = divmod(rem, 60) return f'{hour_count}h, {minute_count}min, {second_count}s' def create_activity_popup(activity): df = activity['gpx_df'] attributes = { 'Date': { 'value': df['Time'][df.index[0]].strftime("%m/%d/%Y"), 'icon': 'calendar' }, 'Start': { 'value': df['Time'][df.index[0]].strftime("%H:%M:%S"), 'icon': 'clock' }, 'End': { 'value': df['Time'][df.index[-1]].strftime("%H:%M:%S"), 'icon': 'flag-checkered' }, 'Duration': { 'value': timedelta_formatter(df['Time'][df.index[-1]]-df['Time'][df.index[0]]), 'icon': 'stopwatch' }, 'Distance': { 'value': f"{np.round(df['Cum_Distance'][df.index[-1]] / 1000, 2)} km", 'icon': 'arrows-left-right' }, 'Average Speed': { 'value': f'{np.round(df.Speed.mean() * 3.6, 2)} km/h', 'icon': 'gauge-high' }, 'Max. Elevation': { 'value': f'{np.round(df.Elevation.max(), 2)} m', 'icon': 'mountain' }, 'Uphill': { 'value': f"{np.round(df[df['Elevation_Diff']>0]['Elevation_Diff'].sum(), 2)} m", 'icon': 'arrow-trend-up' }, 'Downhill': { 'value': f"{np.round(abs(df[df['Elevation_Diff']<0]['Elevation_Diff'].sum()), 2)} m", 'icon': 'arrow-trend-down' }, } html = f"<h4>{activity['name'].upper()}</h4>" for attribute in attributes: html += f'<i class="fa-solid fa-{attributes[attribute]["icon"]}" title="{attribute}"> {attributes[attribute]["value"]}</i></br>' return folium.Popup(html, max_width=300) feature_groups = {} for activity_type in activities: color = ACTIVITY_TYPES[activity_type]['color'] icon = ACTIVITY_TYPES[activity_type]['icon'] for activity_group in activities[activity_type]: # create and store feature groups # this allows different activity types in the same feature group if activity_group not in feature_groups: # create new feature group fg = folium.FeatureGroup(name=activity_group, show=True) feature_groups[activity_group] = fg map.add_child(fg) else: # use existing fg = feature_groups[activity_group] for activity in activities[activity_type][activity_group]: # create line on map points = activity['points'] line = folium.PolyLine(points, color=color, weight=4.5, opacity=.5) fg.add_child(line) # create marker marker = folium.Marker(points[0], popup=create_activity_popup(activity), icon=folium.Icon(color=color, icon_color='white', icon=icon, prefix='fa')) fg.add_child(marker) map.add_child(folium.LayerControl(position='bottomright')) folium_plugins.Fullscreen(position='topright').add_to(map) map
  • 我们将使用 Foliums FeatureGroup 概念根据组名称对路径进行分组。这将使我们能够稍后显示和隐藏某些活动组。
  • 现在,我们将迭代解析的数据并使用 Folium 的 PolyLine 和 Marker 对象在地图上绘制每条路径。折线将代表实际路线,而标记将充当每个活动的起点。当您单击标记时,弹出窗口将显示相应路径的相关信息。

结论

在这次探索和绘图之旅中,我们学习了如何使用 Python 和 Folium 将普通的 GPX 文件转换为动态的交互式地图。现在,您可以重温您的户外探险,庆祝您的进步,并为下一阶段的旅程保持动力。


但是,有很多方法可以自定义、扩展和改进地图。因此,获取您的 GPX 文件,启动 Jupyter Notebook,让您过去的户外活动在地图上栩栩如生!

快乐映射!

下一步是什么?

请继续关注,因为还有更多内容即将推出:


  • 使用 AWS 部署带有地图的网站
  • 使用 Python 和 Plotly 绘制海拔和速度剖面图
  • 用一路上拍摄的照片来增强步道的美感
  • 以及更多

参考


也发布在这里