sábado, 24 de enero de 2026

Análisis geoespacial con Python - Parte 10 - Integrando Todo

Análisis Geoespacial en Python: Integrando Todo
Python // Geospatial Analysis

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.

MÓDULO_01

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

MAPA DE RUTA
Visualización del recorrido sobre un mapa base con hillshade
PERFIL DE ELEVACIÓN
Gráfico mostrando cambios de altitud a lo largo de la ruta
DATOS METEOROLÓGICOS
Condiciones climáticas durante el registro de la ruta
Arquitectura del Sistema GPS Reporter
GPX File Input
Parse XML
Extract Coords
Calculate BBox
WMS Basemap
SRTM Elevation
Weather API
PDF Report Output
MÓDULO_02

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

🐍
Python 3.6+
🖼️
PIL/Pillow
📊
NumPy
📈
pygooglechart
📄
FPDF
🏔️
SRTM.py
🌦️
Dark Sky API
💾
8GB RAM

Instalación de Dependencias

terminal
# 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
El módulo 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.
MÓDULO_03

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

gpx_reporter.py
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

logging_setup.py
# 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)
Ejemplo de salida del logger
2024-01-15 10:23:45 - GPX-Reporter - Parsing GPX file: route.gpx
2024-01-15 10:23:46 - GPX-Reporter - Downloading basemap
2024-01-15 10:23:52 - GPX-Reporter - Retrieving SRTM elevation data
MÓDULO_04

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

utilities.py
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

world2pixel.py
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

haversine.py
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
Flujo de Conversión de Coordenadas
GPS (Lat/Lon)
— ll2m() —→
Metros (X/Y)
— world2pixel() —→
Píxeles (px/py)
MÓDULO_05

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

config.py
# 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

parse_gpx.py
# 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

bounding_box.py
# 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)
MÓDULO_06

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

download_basemap.py
# 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

download_elevation.py
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")
El módulo SRTM.py descarga los datos que necesita. Si descargas datos de diferentes áreas, limpia el caché ubicado en ~/.srtm. Esta parte puede tomar 2-3 minutos.
MÓDULO_07

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

draw_track.py
# 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

locate_photos.py
# 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))
MÓDULO_08

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

elevation_chart.py
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))
MÓDULO_09

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

weather_api.py
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"]
MÓDULO_10

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

pdf_report.py
# 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')
MÓDULO_11

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.

01. Configurar módulo de logging
02. Establecer funciones utilitarias
03. Parsear archivo de datos GPX
04. Calcular bounding box de la ruta
05. Aplicar buffer al bounding box
06. Convertir coordenadas a metros
07. Descargar mapa base OSM
08. Descargar datos de elevación SRTM
09. Crear hillshade de elevación
10. Mezclar hillshade con mapa base
11. Dibujar track GPS en el mapa
12. Dibujar puntos de inicio y fin
13. Localizar fotos geotaggeadas
14. Guardar imagen del mapa
15. Calcular marcadores de milla
16. Construir gráfico de elevación
17. Obtener datos meteorológicos
18. Ensamblar componentes del PDF
19. Generar reporte PDF final

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.

RECURSOS

Enlaces y Descargas

ARCHIVO GPX
MÓDULO SRTM
DARK SKY API

Basado en "Python Geospatial Analysis" // Capítulo 10: Putting It All Together

Joel Lawhead // Packt Publishing