paint-brush
Cómo utilicé Python y Folium para visualizar mis actividades al aire libreby@lukaskrimphove
7,282
7,282

Cómo utilicé Python y Folium para visualizar mis actividades al aire libre

Lukas Krimphove14m2023/09/06
Read on Terminal Reader

¡Embárcate en una expedición de exploración y mapeo! Aprenda cómo darle vida a sus archivos GPX y crear mapas interactivos usando Folium.
featured image - Cómo utilicé Python y Folium para visualizar mis actividades al aire libre
Lukas Krimphove HackerNoon profile picture
0-item

Sueño con hacer una caminata desde Munich hasta Venecia y cruzar los hermosos Alpes. Pero como todavía tengo que vivir mi vida cotidiana, mi viaje tiene que constar de múltiples etapas, con semanas, meses o incluso años entre cada aventura. Eso está bien, ya que se trata más del viaje que del destino. Sin embargo, siempre quise una manera de recorrer visualmente estos caminos, para ver hasta dónde he llegado y qué tan cerca estoy de mi objetivo. Quería una forma de celebrar el progreso que he logrado y motivarme para seguir adelante.


Afortunadamente, muchas aplicaciones deportivas y para actividades al aire libre, como Adidas Running, Komoot, Strava y otras, nos permiten exportar nuestras actividades como archivos GPX. Sin embargo, no hay ningún lugar adonde ir con esos archivos GPX.


Ahí es donde entran en juego Python y Folium . Recientemente me topé con Folium, una poderosa biblioteca de Python para crear mapas interactivos. Puede incorporar fácilmente datos geográficos, como archivos GPX, y permite la personalización y exploración. Aprovechando la gran cantidad de datos de GPS que reuní, comencé a experimentar con Folium para crear un mapa que diera vida a mis excursiones al aire libre.


Después de investigar un poco y realizar muchas pruebas, se me ocurrió un mapa que me permite revisar mis actividades pasadas al aire libre:

Entonces, si eres como yo y tienes un tesoro de datos de GPS sin ningún propósito, te preguntarás cómo llegué allí. Entonces, en este artículo, explicaré cómo darle vida a sus archivos GPX.


¡Embárcate en una expedición de exploración y mapeo!

Primeros pasos con Jupyter Notebook

Para comenzar esta aventura, usaremos Jupyter Notebook. ¿Por qué Jupyter Notebook? Es un fantástico entorno informático interactivo que nos permite combinar código, visualizaciones y texto, lo que lo hace perfecto para experimentar con nuestros datos y la biblioteca Folium.


Si aún no ha instalado Jupyter Notebook, siga las instrucciones en su sitio web oficial . Una vez instalado, puede crear un nuevo Jupyter Notebook y prepararse para su viaje.

Análisis de archivos GPX para datos de senderos

A continuación, necesitamos la materia prima para nuestro mapa: los archivos GPX. GPX (formato de intercambio GPS) es un formato de archivo ampliamente utilizado que almacena datos de ubicación, como latitud, longitud, elevación y hora, lo que lo hace ideal para rastrear actividades al aire libre.


Si eres un ávido excursionista, corredor, ciclista o esquiador, es probable que ya hayas seguido varias excursiones utilizando una aplicación deportiva o para actividades al aire libre. Muchas de esas aplicaciones te permiten exportar tus actividades en formato GPX. Así que reúne esos archivos GPX y ¡comencemos!


En nuestro código Python, usaremos la biblioteca gpxpy para analizar los archivos GPX y extraer los datos esenciales del sendero, como latitud, longitud, elevación, velocidad y distancia. La función parse_gpx() hará todo el trabajo pesado por nosotros:


 ### 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

Eso nos deja con todos los datos necesarios de una actividad: una lista de todas las coordenadas GPS y un Pandas DataFrame que contiene todo tipo de métricas.

Trazar senderos GPX en el mapa interactivo

Con nuestros datos de GPS ahora organizados en un Pandas DataFrame, podemos visualizar nuestras actividades al aire libre en un mapa interactivo. Folium hace que esta tarea sea muy sencilla:


 ### 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' } }
  • Crearemos un mapa centrado en una ubicación específica (puede elegir una o dejar que el código determine el centro en función de sus datos).
  • Asignaremos colores e íconos únicos a cada tipo de actividad, como caminar, correr, andar en bicicleta o esquiar. El diccionario ACTIVITY_TYPES nos ayudará con esto.
 ### 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
  • Usaremos los conceptos de Foliums FeatureGroup para agrupar los senderos según su nombre de grupo. Eso nos permitirá mostrar y ocultar ciertos grupos de actividades más adelante.
  • Ahora, recorreremos nuestros datos analizados y trazaremos cada sendero en el mapa utilizando los objetos PolyLine y Marker de Folium. La PolyLine representará el sendero real, mientras que el Marcador actuará como punto de partida para cada actividad. Al hacer clic en un marcador, una ventana emergente mostrará información relevante sobre el sendero correspondiente.

Conclusión

En este viaje de exploración y mapeo, hemos aprendido cómo usar Python y Folium para transformar archivos GPX mundanos en un mapa dinámico e interactivo. Ahora puedes revivir tus aventuras al aire libre, celebrar tu progreso y mantenerte motivado para la siguiente etapa de tu viaje.


Sin embargo, hay muchas formas de personalizar, ampliar y mejorar su mapa. ¡Tome sus archivos GPX, inicie Jupyter Notebook y deje que sus actividades pasadas al aire libre cobren vida en el mapa!

¡Feliz mapeo!

¿Que sigue?

Estén atentos porque hay mucho más por venir:


  • implementar un sitio web con su mapa usando AWS
  • Trazar perfiles de elevación y velocidad usando Python y Plotly.
  • Mejorando senderos con fotografías tomadas en el camino.
  • y mucho más

Referencias

  • Todo el código, incluido Jupyter Notebook, está en mi GitHub .
  • Mientras investigaba sobre Folium, encontré un excelente artículo de Patrick, quien hizo lo mismo que yo planeaba hacer. Su trabajo fue una excelente base para desarrollar mi solución, así que compruébelo.
  • Cuaderno Jupyter
  • folio
  • pandas

También publicado aquí .