El problema: navegar evitando ciertas zonas
Todos los GPS te dan la ruta más rápida. Pero a veces quieres evitar ciertas zonas - una obra que bloquea la calle cada mañana, un barrio que prefieres rodear, una carretera donde sabes que hay un radar.
Esa fue la necesidad que dio lugar a Wayline: una PWA de navegación GPS donde defines tus zonas de evitación, y la aplicación recalcula automáticamente rutas que las rodean.
Aquí están las decisiones técnicas que tomé, y por qué.
El stack: ¿por qué no una app nativa?
Para nav GPS, el reflejo es React Native o Flutter. Elegí PWA Next.js + Mapbox por varias razones:
- Sin validación App Store: despliegue instantáneo, actualizaciones sin review
- Geolocalización nativa vía la API Web:
navigator.geolocationfunciona igual de bien que una app nativa en iOS y Android recientes - Mapbox GL JS: motor de renderizado vectorial ultrarrápido, mismas capacidades que los SDK nativos
- Service Worker: puedo cachear el mapa y las rutas para uso offline
- Sin fricción de instalación: el usuario abre la URL, la añade a la pantalla de inicio, listo
El único trade-off real: sin acceso a notificaciones push iOS fuera de PWA instalada. Para Wayline no es crítico - el GPS se usa al salir, no en segundo plano.
Calcular una ruta con evitación de zonas
Es el corazón del proyecto. Mapbox ofrece una API de routing (Directions API) que toma waypoints y devuelve la ruta óptima. Pero no sabe "evitar tal polígono".
El truco: calculamos una ruta de base, miramos si atraviesa una zona de evitación, y si es así, insertamos un waypoint que fuerza el desvío.
Para verificar la intersección entre la línea de la ruta y el polígono, uso Turf.js:
import * as turf from '@turf/turf';
function routeIntersectsZone(routeGeoJSON, zonePolygon) {
return turf.booleanIntersects(routeGeoJSON, zonePolygon);
}
function findDetourWaypoint(zonePolygon) {
const center = turf.centroid(zonePolygon);
const bbox = turf.bbox(zonePolygon);
// Coloca el waypoint ligeramente fuera del norte de la zona
return [bbox[0] - 0.01, bbox[3] + 0.01];
}
El algoritmo simplificado:
- Llamada a Mapbox Directions con origen + destino
- Para cada zona definida: comprobar si la ruta la atraviesa
- Si es así, calcular un waypoint de desvío
- Re-llamada a Directions con el waypoint insertado
- Devolver la ruta final
Sigue siendo 1-2 llamadas API por cálculo de ruta, pero la experiencia es fluida (<300ms de media).
Almacenamiento local con SQLite
Para las zonas de evitación y el historial de rutas, elegí SQLite del lado servidor en lugar de Postgres o un servicio gestionado. Por qué:
- Wayline es un proyecto personal, no necesita escalar a miles de usuarios
- SQLite corre en el mismo proceso Node.js (
better-sqlite3), zero connection overhead - Backups simples: un solo archivo
.db, puedo hacer rsync al NAS - Lectura casi instantánea para las zonas del usuario (típicamente 5-20 zonas)
Para las consultas espaciales (¿esta zona contiene este punto?), Turf.js hace el trabajo del lado servidor. No hace falta PostGIS.
const Database = require('better-sqlite3');
const db = new Database('wayline.db');
db.exec(`
CREATE TABLE IF NOT EXISTS zones (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT,
geojson TEXT NOT NULL,
created_at INTEGER
);
`);
const getUserZones = db.prepare('SELECT * FROM zones WHERE user_id = ?');
Modo offline con Service Worker
Es el verdadero diferenciador PWA vs app nativa clásica. Con un service worker, puedo cachear:
- El shell de la app (HTML/CSS/JS)
- Los tiles Mapbox ya visitados (Mapbox GL gestiona esto nativamente con un cache configurado)
- Las zonas del usuario (almacenadas en
localStorage) - La última ruta calculada
Resultado: el usuario puede seguir viendo su mapa y su ruta incluso al perder red, lo que ocurre regularmente en zona rural o sótano.
Setup mínimo del service worker (con next-pwa):
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.mapbox\.com\/styles\//,
handler: 'CacheFirst',
options: { cacheName: 'mapbox-tiles', expiration: { maxAgeSeconds: 7 * 24 * 60 * 60 } }
}
]
});
Las trampas encontradas
1. navigator.geolocation puede mentir. En algunos navegadores móviles, la precisión varía de 5m a 500m de un frame al otro. Tuve que implementar un Kalman filter simplificado para suavizar el tracking.
2. Los tiles Mapbox son caros más allá del free tier. Si Wayline tuviera tráfico masivo, pasaría a un proveedor de tiles open-source (Stadia Maps, Protomaps). Por ahora, el free tier basta de sobra.
3. iOS Safari y el service worker. Apple tiene sus propias reglas: los service workers se descargan agresivamente, el cache no está garantizado. Para usos críticos (un ciclista que debe poder consultar su ruta), recomiendo doblar con localStorage.
El balance
| Métrica | Valor |
|---|---|
| Bundle JS inicial | ~140 KB gzip |
| Time to Interactive | <2s en 4G |
| Recálculo de ruta con evitación | 250-350 ms |
| Funcionamiento offline | ✅ Sí (mapa + última ruta) |
| Almacenamiento del lado usuario | ~3 MB tras varias rutas |
Lo que saco de esto
Para el 95% de los casos de uso GPS, una PWA bien hecha reemplaza a una app nativa. El único interés real de una app nativa para un proyecto de navegación es el uso en segundo plano continuo (tipo Waze hablándote al volante). Si no necesitas eso, la PWA es más rápida de desarrollar, más fácil de desplegar, y ofrece una experiencia comparable.
Mapbox + Turf.js es una combinación potente: Mapbox para el render y el routing estándar, Turf.js para todo lo que sale de su API (intersecciones, buffers, distancias custom). Con estos dos ladrillos, puedes construir prácticamente cualquier lógica geoespacial en el navegador.
