7 min di lettura

Costruire una PWA di navigazione GPS con Next.js + Mapbox (case study Wayline)

PWANext.jsMapboxTurf.jsGPS
Costruire una PWA di navigazione GPS con Next.js + Mapbox (case study Wayline)

Il problema: navigare evitando certe zone

Tutti i GPS ti danno il percorso più veloce. Ma a volte vuoi evitare certe zone - un cantiere che blocca la strada ogni mattina, un quartiere che preferisci aggirare, una strada dove sai che c'è un autovelox.

È questa esigenza che ha dato origine a Wayline: una PWA di navigazione GPS dove definisci le tue zone di evitamento, e l'applicazione ricalcola automaticamente percorsi che le aggirano.

Ecco le scelte tecniche che ho fatto, e perché.

Lo stack: perché non un'app nativa?

Per la nav GPS, il riflesso è React Native o Flutter. Ho scelto PWA Next.js + Mapbox per diversi motivi:

  • Niente validazione App Store: deploy istantaneo, aggiornamenti senza review
  • Geolocalizzazione nativa via API Web: navigator.geolocation funziona bene quanto un'app nativa su iOS e Android recenti
  • Mapbox GL JS: motore di rendering vettoriale ultra-rapido, stesse capacità degli SDK nativi
  • Service Worker: posso cachare la mappa e i percorsi per l'uso offline
  • Niente attrito di installazione: l'utente apre l'URL, lo aggiunge alla home, fatto

L'unico vero trade-off: niente accesso alle notifiche push iOS fuori dalla PWA installata. Per Wayline non è critico - il GPS si usa uscendo, non in background.

Calcolare un percorso con evitamento di zone

È il cuore del progetto. Mapbox propone un'API di routing (Directions API) che prende dei waypoint e restituisce il percorso ottimale. Ma non sa "evitare tale poligono".

Il trucco: si calcola un percorso di base, si guarda se attraversa una zona di evitamento, e in caso affermativo, si inserisce un waypoint che forza la deviazione.

Per verificare l'intersezione tra la linea del percorso e il poligono, 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);
  // Posiziona il waypoint leggermente all'esterno del nord della zona
  return [bbox[0] - 0.01, bbox[3] + 0.01];
}

L'algoritmo semplificato:

  1. Chiamata a Mapbox Directions con origine + destinazione
  2. Per ogni zona definita: check se il percorso la attraversa
  3. Se sì, calcola un waypoint di deviazione
  4. Ri-chiamata a Directions con il waypoint inserito
  5. Ritorna il percorso finale

Restano 1-2 chiamate API per calcolo di percorso, ma l'esperienza è fluida (<300ms in media).

Archiviazione locale con SQLite

Per le zone di evitamento e lo storico dei percorsi, ho scelto SQLite lato server invece di Postgres o un servizio gestito. Perché:

  • Wayline è un progetto personale, non serve scalare a migliaia di utenti
  • SQLite gira nello stesso processo Node.js (better-sqlite3), zero connection overhead
  • Backup semplici: un solo file .db, posso fare rsync sul NAS
  • Lettura quasi istantanea per le zone dell'utente (tipicamente 5-20 zone)

Per le query spaziali (questa zona contiene questo punto?), Turf.js fa il lavoro lato server. Non serve 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 = ?');

Modalità offline con Service Worker

È il vero differenziatore PWA vs app nativa classica. Con un service worker, posso cachare:

  • La shell dell'app (HTML/CSS/JS)
  • I tile Mapbox già visitati (Mapbox GL gestisce questo nativamente con una cache configurata)
  • Le zone dell'utente (memorizzate in localStorage)
  • L'ultimo percorso calcolato

Risultato: l'utente può continuare a vedere la sua mappa e il suo percorso anche in perdita di rete, cosa che capita regolarmente in zona rurale o sotterraneo.

Setup minimo 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 } }
    }
  ]
});

Le trappole incontrate

1. navigator.geolocation può mentire. Su alcuni browser mobili, la precisione varia da 5m a 500m da un frame all'altro. Ho dovuto implementare un Kalman filter semplificato per smorzare il tracking.

2. I tile Mapbox sono cari oltre il free tier. Se Wayline avesse traffico massiccio, passerei a un fornitore di tile open-source (Stadia Maps, Protomaps). Per ora, il free tier basta abbondantemente.

3. iOS Safari e il service worker. Apple ha le sue regole: i service worker vengono scaricati aggressivamente, la cache non è garantita. Per usi critici (un ciclista che deve poter consultare il suo percorso), raccomando di raddoppiare con localStorage.

Il bilancio

MetricaValore
Bundle JS iniziale~140 KB gzip
Time to Interactive<2s su 4G
Ricalcolo percorso con evitamento250-350 ms
Funzionamento offline✅ Sì (mappa + ultimo percorso)
Archiviazione lato utente~3 MB dopo diversi percorsi

Cosa ne traggo

Per il 95% dei casi d'uso GPS, una PWA ben fatta sostituisce un'app nativa. L'unico vero interesse di un'app nativa per un progetto di navigazione è l'uso in background continuo (tipo Waze che ti parla in guida). Se non hai bisogno di questo, la PWA è più rapida da sviluppare, più facile da deployare, e offre un'esperienza paragonabile.

Mapbox + Turf.js è una combinazione potente: Mapbox per il rendering e il routing standard, Turf.js per tutto ciò che esce dalla loro API (intersezioni, buffer, distanze custom). Con questi due mattoni, puoi costruire praticamente qualsiasi logica geospaziale nel browser.