7 Min. Lesezeit

Eine GPS-Navigations-PWA mit Next.js + Mapbox bauen (Case Study Wayline)

PWANext.jsMapboxTurf.jsGPS
Eine GPS-Navigations-PWA mit Next.js + Mapbox bauen (Case Study Wayline)

Das Problem: navigieren und dabei bestimmte Zonen meiden

Jedes GPS gibt dir die schnellste Route. Aber manchmal willst du bestimmte Zonen meiden - eine Baustelle, die jeden Morgen die Straße blockiert, ein Viertel, das du lieber umfährst, eine Straße, auf der du weißt, dass ein Blitzer steht.

Genau dieser Bedarf hat Wayline ins Leben gerufen: eine GPS-Navigations-PWA, in der du deine Vermeidungszonen definierst, und die Anwendung berechnet automatisch Routen neu, die sie umgehen.

Hier sind die technischen Entscheidungen, die ich getroffen habe, und warum.

Der Stack: warum keine native App?

Für GPS-Navigation ist der Reflex React Native oder Flutter. Ich habe PWA Next.js + Mapbox aus mehreren Gründen gewählt:

  • Keine App-Store-Validierung: sofortiges Deployment, Updates ohne Review
  • Native Geolokalisierung über die Web-API: navigator.geolocation funktioniert genauso gut wie eine native App auf aktuellen iOS und Android
  • Mapbox GL JS: ultraschnelle Vektor-Rendering-Engine, gleiche Fähigkeiten wie native SDKs
  • Service Worker: ich kann Karte und Routen für die Offline-Nutzung cachen
  • Keine Install-Reibung: der Nutzer öffnet die URL, fügt sie zum Homescreen hinzu, fertig

Der einzige echte Trade-off: kein Zugriff auf iOS-Push-Notifications außerhalb einer installierten PWA. Für Wayline nicht kritisch - das GPS wird beim Rausgehen genutzt, nicht im Hintergrund.

Eine Route mit Zonenvermeidung berechnen

Das ist der Kern des Projekts. Mapbox bietet eine Routing-API (Directions API), die Waypoints nimmt und die optimale Route zurückgibt. Aber sie kann nicht "dieses Polygon meiden".

Der Trick: man berechnet eine Basis-Route, schaut, ob sie eine Vermeidungszone durchquert, und falls ja, fügt man einen Waypoint ein, der den Umweg erzwingt.

Um die Schnittmenge zwischen Routenlinie und Polygon zu prüfen, nutze ich 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);
  // Platziert den Waypoint leicht außerhalb der Nordseite der Zone
  return [bbox[0] - 0.01, bbox[3] + 0.01];
}

Der vereinfachte Algorithmus:

  1. Aufruf von Mapbox Directions mit Ursprung + Ziel
  2. Für jede definierte Zone: prüfen, ob die Route sie durchquert
  3. Falls ja, einen Detour-Waypoint berechnen
  4. Erneuter Aufruf von Directions mit dem eingefügten Waypoint
  5. Finale Route zurückgeben

Bleibt bei 1-2 API-Aufrufen pro Routenberechnung, aber das Erlebnis ist flüssig (<300ms im Schnitt).

Lokale Speicherung mit SQLite

Für die Vermeidungszonen und die Routen-Historie habe ich serverseitiges SQLite gewählt statt Postgres oder einem Managed Service. Warum:

  • Wayline ist ein persönliches Projekt, kein Bedarf, auf Tausende User zu skalieren
  • SQLite läuft im selben Node.js-Prozess (better-sqlite3), zero connection overhead
  • Einfache Backups: eine einzige .db-Datei, ich kann sie auf den NAS rsyncen
  • Quasi-instantane Lesezugriffe für die Zonen des Nutzers (typischerweise 5-20 Zonen)

Für räumliche Abfragen (enthält diese Zone diesen Punkt?) erledigt Turf.js den Job serverseitig. Kein PostGIS nötig.

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 = ?');

Offline-Modus mit Service Worker

Das ist der echte Differenzierer PWA vs klassische native App. Mit einem Service Worker kann ich cachen:

  • Die App-Shell (HTML/CSS/JS)
  • Bereits besuchte Mapbox-Tiles (Mapbox GL handhabt das nativ mit einem konfigurierten Cache)
  • Die Zonen des Nutzers (in localStorage gespeichert)
  • Die zuletzt berechnete Route

Resultat: der Nutzer kann seine Karte und seine Route weiter sehen, auch bei Netzverlust, was regelmäßig in ländlichen Gebieten oder im Untergeschoss vorkommt.

Minimales Service-Worker-Setup (mit 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 } }
    }
  ]
});

Die Stolperfallen

1. navigator.geolocation kann lügen. Auf manchen mobilen Browsern variiert die Genauigkeit zwischen 5m und 500m von einem Frame zum nächsten. Ich musste einen vereinfachten Kalman-Filter implementieren, um das Tracking zu glätten.

2. Mapbox-Tiles werden jenseits des Free Tiers teuer. Hätte Wayline massiven Traffic, würde ich auf einen Open-Source-Tile-Anbieter wechseln (Stadia Maps, Protomaps). Aktuell reicht das Free Tier locker.

3. iOS Safari und der Service Worker. Apple hat seine eigenen Regeln: Service Worker werden aggressiv entladen, der Cache ist nicht garantiert. Für kritische Use Cases (ein Radfahrer, der seine Route abrufen können muss) empfehle ich, mit localStorage zu doppeln.

Die Bilanz

MetrikWert
Initiales JS-Bundle~140 KB gzip
Time to Interactive<2s über 4G
Routen-Neuberechnung mit Vermeidung250-350 ms
Offline-Betrieb✅ Ja (Karte + letzte Route)
User-seitige Speicherung~3 MB nach mehreren Routen

Was ich daraus mitnehme

Für 95% der GPS-Use-Cases ersetzt eine gut gemachte PWA eine native App. Der einzige echte Vorteil einer nativen App für ein Navigationsprojekt ist die kontinuierliche Hintergrund-Nutzung (à la Waze, der dir beim Fahren etwas sagt). Brauchst du das nicht, ist die PWA schneller zu entwickeln, leichter zu deployen, und bietet ein vergleichbares Erlebnis.

Mapbox + Turf.js ist eine kraftvolle Kombination: Mapbox für Rendering und Standard-Routing, Turf.js für alles, was außerhalb ihrer API liegt (Schnittmengen, Buffer, Custom-Distanzen). Mit diesen beiden Bausteinen kannst du nahezu jede geo-räumliche Logik im Browser bauen.