Je rêve de faire une randonnée de Munich à Venise et de parcourir les magnifiques Alpes. Mais comme je dois encore vivre mon quotidien, mon voyage doit être composé de plusieurs étapes, avec des semaines, des mois, voire des années entre chaque aventure. Ce n'est pas grave, car il s'agit plus du voyage que de la destination. Cependant, j'ai toujours voulu un moyen de retracer visuellement ces chemins, de voir jusqu'où je suis arrivé et à quel point je suis proche de mon objectif. Je voulais un moyen de célébrer les progrès que j'ai réalisés et de me motiver à aller plus loin.
Heureusement, de nombreuses applications de plein air et de sport, comme Adidas Running, Komoot, Strava et autres, nous permettent gracieusement d'exporter nos activités sous forme de fichiers GPX. Cependant, il n’y a nulle part où aller avec ces fichiers GPX.
C'est là que Python et Folium entrent en jeu. Je suis récemment tombé sur Folium, une puissante bibliothèque Python permettant de créer des cartes interactives. Il peut facilement intégrer des données géographiques, telles que les fichiers GPX, et permet la personnalisation et l'exploration. En m'appuyant sur la richesse des données GPS que j'ai collectées, j'ai commencé à expérimenter avec Folium pour créer une carte qui donne vie à mes excursions en plein air.
Après quelques recherches et de nombreux tests, j'ai abouti à une carte qui me permet de revisiter mes activités outdoor passées :
Donc, si vous êtes comme moi et que vous possédez un trésor de données GPS sans aucun but, vous vous demandez peut-être comment j'en suis arrivé là. Ainsi, dans cet article, je vais vous expliquer comment donner vie à vos fichiers GPX.
Embarquons pour une expédition d'exploration et de cartographie !
Pour commencer cette aventure, nous utiliserons Jupyter Notebook. Pourquoi le bloc-notes Jupyter ? Il s'agit d'un fantastique environnement informatique interactif qui nous permet de combiner du code, des visualisations et du texte, ce qui le rend parfait pour expérimenter nos données et la bibliothèque Folium.
Si vous n'avez pas encore installé Jupyter Notebook, suivez les instructions sur leur site officiel . Une fois installé, vous pouvez créer un nouveau bloc-notes Jupyter et vous préparer pour votre voyage.
Ensuite, nous avons besoin de la matière première pour notre carte : les fichiers GPX. GPX (GPS Exchange Format) est un format de fichier largement utilisé qui stocke des données de localisation, telles que la latitude, la longitude, l'altitude et l'heure, ce qui le rend idéal pour suivre les activités de plein air.
Si vous êtes un randonneur, un coureur, un cycliste ou un skieur passionné, il est probable que vous ayez déjà suivi diverses excursions à l'aide d'une application de plein air ou de sport. De nombreuses applications vous permettent d'exporter vos activités au format GPX. Alors rassemblez ces fichiers GPX et commençons !
Dans notre code Python, nous utiliserons la bibliothèque gpxpy pour analyser les fichiers GPX et extraire les données essentielles du sentier, telles que la latitude, la longitude, l'altitude, la vitesse et la distance. La fonction parse_gpx() fera tout le gros du travail à notre place :
### 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
Cela nous laisse avec toutes les données nécessaires d'une activité : une liste de toutes les coordonnées GPS et un Pandas DataFrame contenant toutes sortes de métriques.
Grâce à nos données GPS désormais organisées dans un Pandas DataFrame, nous pouvons visualiser nos activités de plein air sur une carte interactive. Folium facilite cette tâche :
### 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
Au cours de ce voyage d'exploration et de cartographie, nous avons appris à utiliser Python et Folium pour transformer des fichiers GPX banals en une carte dynamique et interactive. Désormais, vous pouvez revivre vos aventures en plein air, célébrer vos progrès et rester motivé pour la prochaine étape de votre voyage.
Cependant, il existe de nombreuses façons de personnaliser, d'étendre et d'améliorer votre carte. Alors récupérez vos fichiers GPX, lancez Jupyter Notebook et laissez vos activités de plein air passées prendre vie sur la carte !
Bonne cartographie !
Restez à l'écoute car il y a beaucoup plus à venir :
Également publié ici .