私の夢は、ミュンヘンからヴェネツィアまでハイキングし、美しいアルプスをトレッキングすることです。しかし、それでも日常生活を送らなければならないので、私の旅は複数の段階で構成され、各冒険の間には数週間、数か月、場合によっては数年もかかる必要があります。目的地よりも旅そのものなので、それは問題ありません。しかし、私は常にこれらのパスを視覚的にたどって、自分がどこまで到達し、目標にどれだけ近づいているかを確認する方法を望んでいました。私は自分の進歩を祝い、さらに前進するよう自分を奮い立たせる方法が欲しかったのです。
幸いなことに、Adidas Running、Komoot、Strava などの多くのアウトドア アプリやスポーツ アプリでは、アクティビティを GPX ファイルとしてエクスポートできます。ただし、これらの GPX ファイルを保存する場所はありません。
そこで Python とFolium が登場します。私は最近、インタラクティブなマップを作成するための強力な Python ライブラリである Folium に出会いました。 GPX ファイルなどの地理データを簡単に組み込むことができ、カスタマイズや探索が可能になります。収集した豊富な GPS データを利用して、アウトドア旅行に命を吹き込む地図を作成するために Folium の実験を開始しました。
いくつかの調査と多くのテストを経て、過去のアウトドア活動を振り返ることができる地図を思いつきました。
したがって、あなたが私と同じで、何の目的もなく GPS データの宝庫を持っているなら、私がどうやってそこにたどり着いたのか不思議に思うかもしれません。そこでこの記事では、GPX ファイルに命を吹き込む方法を説明します。
探検と地図作成の旅に出かけましょう!
この冒険を始めるには、Jupyter Notebook を使用します。 Jupyter Notebook を選ぶ理由これは、コード、ビジュアライゼーション、テキストを組み合わせることができる素晴らしいインタラクティブ コンピューティング環境で、データと Folium ライブラリを実験するのに最適です。
Jupyter Notebook をまだインストールしていない場合は、公式 Web サイトの手順に従ってください。インストールしたら、新しい Jupyter Notebook を作成して、旅の準備を始めることができます。
次に、マップの原材料である GPX ファイルが必要です。 GPX (GPS Exchange Format) は、緯度、経度、標高、時間などの位置データを保存する広く使用されているファイル形式であり、屋外アクティビティの追跡に最適です。
あなたが熱心なハイカー、ランナー、サイクリスト、またはスキーヤーであれば、アウトドア アプリやスポーツ アプリを使用してさまざまな旅行をすでに追跡している可能性があります。これらのアプリの多くでは、アクティビティを 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 が残ります。
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' } }
### 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
この探索とマッピングの旅で、私たちは Python と Folium を使用してありふれた GPX ファイルを動的でインタラクティブなマップに変換する方法を学びました。これで、アウトドアの冒険を追体験し、進歩を祝い、旅の次の段階に向けてモチベーションを維持することができます。
ただし、マップをカスタマイズ、拡張、改善する方法はたくさんあります。 GPX ファイルを取得し、Jupyter Notebook を起動して、過去のアウトドア アクティビティを地図上で生き生きとさせましょう。
マッピングを楽しんでください。
これからもたくさんのことがあるのでお楽しみに:
ここでも公開されています。