Integrando Todo: GPS Reporter
Construye un sistema completo de reportes GPS combinando todas las técnicas de análisis geoespacial
A lo largo de este capítulo final, integraremos prácticamente todas las técnicas de análisis geoespacial que hemos aprendido para crear un producto del mundo real muy popular: un reporte de análisis de rutas GPS.
Estos reportes son comunes en docenas de aplicaciones móviles, relojes GPS, sistemas de navegación y otras herramientas basadas en GPS. Aplicaciones fitness como RunKeeper, MapMyRun, Strava y Nike Plus utilizan reportes similares para presentar datos de ejercicio rastreados por GPS.
Anatomía de un Reporte GPS
Un GPS típicamente registra ubicación, tiempo y elevación. A partir de estos valores, podemos derivar una gran cantidad de información auxiliar sobre lo que ocurrió a lo largo de la ruta donde se grabaron los datos.
Elementos Comunes del Reporte
Requisitos Técnicos
Para construir nuestro GPX-Reporter necesitamos configurar el entorno con las bibliotecas necesarias. Este programa tiene casi 500 líneas de código, nuestro más largo hasta ahora.
Dependencias del Sistema
Instalación de Dependencias
# Instalar el módulo SRTM para datos de elevación pip install srtm.py # Instalar las demás dependencias pip install pillow numpy pygooglechart fpdf
SRTM.py trabaja con datos de elevación casi globales recolectados durante la misión Shuttle Radar Topography Mission (SRTM) en el año 2000 por el transbordador espacial Endeavour.
Configuración Inicial
El programa comienza con las declaraciones de importación seguidas de la configuración del módulo de logging de Python.
Importaciones del Sistema
from xml.dom import minidom import json import urllib.request import math import time import logging import numpy as np import srtm from pygooglechart import SimpleLineChart, Axis import fpdf import glob from PIL import Image, ImageFilter, ImageEnhance, ImageDraw from PIL.ExifTags import TAGS
Configuración del Logger
# Configurar módulo de logging level = logging.DEBUG formatter = logging.Formatter("%(asctime)s - %(name)s - %(message)s") log = logging.getLogger("GPX-Reporter") log.setLevel(level) console = logging.StreamHandler() console.setLevel(level) console.setFormatter(formatter) log.addHandler(console)
2024-01-15 10:23:46 - GPX-Reporter - Downloading basemap
2024-01-15 10:23:52 - GPX-Reporter - Retrieving SRTM elevation data
Funciones Utilitarias
El programa utiliza varias funciones utilitarias que manejan conversiones de coordenadas, cálculos de distancia y recuperación de datos de servicios web.
Conversión Lat/Lon a Metros
def ll2m(lat, lon): """Convierte latitud/longitud a metros""" x = lon * 20037508.34 / 180.0 y = math.log(math.tan((90.0 + lat) * math.pi / 360.0)) / (math.pi / 180.0) y = y * 20037508.34 / 180 return (x, y)
Conversión de Coordenadas a Píxeles
def world2pixel(x, y, w, h, bbox): """Convierte coordenadas geoespaciales a píxeles""" minx, miny, maxx, maxy = bbox xdist = maxx - minx ydist = maxy - miny xratio = w / xdist yratio = h / ydist px = w - ((maxx - x) * xratio) py = (maxy - y) * yratio return int(px), int(py)
Fórmula Haversine para Distancias
def haversine(x1, y1, x2, y2): """Fórmula de distancia Haversine""" x_dist = math.radians(x1 - x2) y_dist = math.radians(y1 - y2) y1_rad = math.radians(y1) y2_rad = math.radians(y2) a = math.sin(y_dist/2)**2 + math.sin(x_dist/2)**2 \ * math.cos(y1_rad) * math.cos(y2_rad) c = 2 * math.asin(math.sqrt(a)) distance = c * (6371/1.609344) # Millas return distance
Parsing del Archivo GPX
El formato GPX (GPS Exchange Format) es el más común para almacenar información de rutas GPS. Es simplemente XML, por lo que usamos el módulo integrado xml.dom.minidom.
Configuración de Variables
# Constantes de conversión deg2rad = 3.141592653589793 / 180.0 rad2deg = 180.0 / 3.141592653589793 # Archivo GPX con la ruta gpx = "route.gpx" # Configuración del servicio WMS de OpenStreetMap osm_WMS = "http://ows.mundialis.de/services/service" osm_lyr = "OSM-WMS" osm_img = "basemap" osm_epsg = 3857 # Parámetros del hillshade azimuth = 315.0 # Dirección del sol altitude = 45.0 # Ángulo del sol z = 5.0 # Exageración de elevación # Dimensiones de imagen w = 800 h = 800
Extracción de Datos GPX
# Parsear el archivo GPX log.info("Parsing GPX file: {}".format(gpx)) xml = minidom.parse(gpx) # Obtener todos los elementos "trkpt" trkpts = xml.getElementsByTagName("trkpt") # Listas para almacenar datos lats, lons, elvs, times = [], [], [], [] for trkpt in trkpts: lat = float(trkpt.attributes["lat"].value) lon = float(trkpt.attributes["lon"].value) lats.append(lat) lons.append(lon) elv = float(trkpt.childNodes[0].firstChild.nodeValue) elvs.append(elv) t = trkpt.childNodes[1].firstChild.nodeValue t = get_local_time(t) times.append(t)
Cálculo del Bounding Box
# Encontrar bounding box de la ruta minx, miny = min(lons), min(lats) maxx, maxy = max(lons), max(lats) # Buffer del bounding box al 20% xdist = maxx - minx ydist = maxy - miny x20 = xdist * 0.2 y20 = ydist * 0.2 minx -= x20 miny -= y20 maxx += x20 maxy += y20 bbox = [minx, miny, maxx, maxy] # Convertir a metros para WMS mminx, mminy = ll2m(miny, minx) mmaxx, mmaxy = ll2m(maxy, maxx)
Descarga de Mapas y Elevación
Descargaremos dos capas de datos: el mapa base de OpenStreetMap y los datos de elevación SRTM para crear el relieve sombreado.
Descarga del Mapa Base
# Descargar el mapa base de OSM log.info("Downloading basemap") wms(mminx, mminy, mmaxx, mmaxy, osm_WMS, osm_lyr, osm_epsg, osm_style, osm_img, w, h)
Descarga de Datos SRTM
log.info("Retrieving SRTM elevation data") # El módulo SRTM usa caché local srt = srtm.get_data() # Obtener imagen de elevación image = srt.get_image( (w, h), (miny, maxy), (minx, maxx), 300, zero_color=zero_clr, min_color=min_clr, max_color=max_clr ) image.save(elv_img + ".png")
Creación del Mapa Topográfico
Combinamos el mapa base con el hillshade para crear un mapa topográfico, y luego dibujamos la ruta GPS sobre él.
Dibujo de la Ruta GPS
# Crear objeto de dibujo topo_draw = ImageDraw.Draw(topo) # Convertir coordenadas GPS a píxeles track_pixels = [] for i in range(len(lats)): px, py = world2pixel(lons[i], lats[i], w, h, bbox) track_pixels.append((px, py)) # Dibujar línea del track topo_draw.line(track_pixels, fill="red", width=3) # Punto de inicio (verde) start_x, start_y = track_pixels[0] topo_draw.ellipse( [start_x-8, start_y-8, start_x+8, start_y+8], fill="green", outline="white" ) # Punto final (rojo) end_x, end_y = track_pixels[-1] topo_draw.ellipse( [end_x-8, end_y-8, end_x+8, end_y+8], fill="red", outline="white" )
Localización de Fotos Geotaggeadas
# Obtener lista de imágenes images = glob.glob("photos/*.jpg") for i in images: e = exif(i) photo_lat, photo_lon = gps(e) photo_x, photo_y = world2pixel(photo_lon, photo_lat, w, h, bbox) # Dibujar ícono de cámara topo_draw.rectangle( [photo_x-12, photo_y-10, photo_x+12, photo_y+10], fill="black" ) topo.save("{}_topo.jpg".format(osm_img))
Perfil de Elevación y Distancia
Usando la API de Google Charts, construimos un gráfico de perfil de elevación con marcadores de distancia en millas.
Creación del Gráfico
log.info("Creating elevation profile chart") chart = SimpleLineChart(600, 300, y_range=[min(elvs), max(elvs)]) # API requiere 3 líneas de datos para colorear chart.add_data([min(elvs)]*2) chart.add_data(elvs) chart.add_data([min(elvs)]*2) chart.set_colours(['000000']) chart.add_fill_range('80C65A', 1, 2) # Calcular distancias entre segmentos distances = [] coords = list(zip(lons, lats)) for i in range(len(coords) - 1): x1, y1 = coords[i] x2, y2 = coords[i + 1] d = haversine(x1, y1, x2, y2) distances.append(d) total = sum(distances) chart.download('{}_profile.png'.format(elv_img))
Datos Meteorológicos
Utilizamos el servicio Dark Sky para recopilar reportes meteorológicos históricos. La API es REST y basada en JSON.
Consulta de API Meteorológica
log.info("Creating weather summary") # Centroide del bounding box centx = minx + ((maxx - minx) / 2) centy = miny + ((maxy - miny) / 2) # API key de Dark Sky api_key = "YOUR API KEY GOES HERE" t = times[-1] # Construir URL de consulta history_req = "https://api.darksky.net/forecast/{}/".format(api_key) history_req += "{},{},{}".format(centy, centx, t) history_req += "?exclude=currently,minutely,hourly,alerts,flags" request = urllib.request.urlopen(history_req) weather_data = request.read() # Parsear datos JSON js = json.loads(weather_data.decode("utf-8")) daily = js["daily"]["data"][0] maxtemp = daily["temperatureMax"] mintemp = daily["temperatureMin"] maxhum = daily["humidity"]
Generación del Reporte PDF
La biblioteca fpdf es simple y de Python puro. Agregamos los elementos del reporte página abajo.
Configuración y Contenido del PDF
# Crear objeto PDF (portrait, pulgadas, carta) pdf = fpdf.FPDF("P", "in", "Letter") pdf.add_page() # Título pdf.set_font('Arial', 'B', 20) pdf.cell(6.25, 1, 'GPX Report', border=0, align="C") # Agregar mapa de ruta pdf.set_font('Arial', style='B', size=14) pdf.cell(w=1.2, h=1, txt="Route Map", align="C") pdf.image("{}_topo.jpg".format(osm_img), 1, 2, 4, 4) # Agregar gráfico de elevación pdf.cell(w=1.2, h=1, txt="Elevation Profile", align="C") pdf.image("{}_profile.png".format(elv_img), 1, 6.5, 4, 2) # Resumen meteorológico pdf.cell(1.2, 1, "Weather Summary", align="C") pdf.cell(1.8, 1, "Min. Temp.: {}".format(mintemp), align="L") pdf.cell(1.8, 1, "Max. Temp.: {}".format(maxtemp), align="L") # Guardar el reporte log.info("Saving report pdf") pdf.output('report.pdf', 'F')
Flujo Completo del Programa
El programa GPX-Reporter sigue una secuencia lógica de 19 pasos que transforman datos GPS crudos en un reporte PDF profesional.
Técnicas Integradas
01 // Parsing XML
Uso de minidom para extraer datos de archivos GPX incluyendo coordenadas, elevación y timestamps.
02 // Servicios WMS
Descarga de mapas base mediante servicios web OGC estándar como OpenStreetMap WMS.
03 // Datos SRTM
Recuperación de datos de elevación global del Shuttle Radar Topography Mission.
04 // Hillshading
Creación de sombreado de relieve usando gradientes NumPy para visualización 3D del terreno.
05 // Procesamiento de Imágenes
Manipulación con PIL/Pillow para blend de capas, dibujo de tracks y anotaciones.
06 // APIs REST/JSON
Consumo de servicios web como Dark Sky para obtener datos meteorológicos históricos.
07 // Gráficos
Generación de perfiles de elevación usando Google Charts API con marcadores de distancia.
08 // Generación de PDF
Creación de reportes profesionales con FPDF incluyendo imágenes, gráficos y texto.