Skip to content
TCdesign-devPublic

About

Trasforma gli avvisi di deviazione di GTT Torino in mappe: quali fermate saltano e dove andare al loro posto.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

136 Commits

Folders and files

Repository files navigation

🚏 DeviaTo

Gli avvisi di deviazione di GTT (Torino) sono scritti in prosa.
DeviaTo li trasforma in una mappa e risponde a una domanda sola: la mia fermata Γ¨ ancora servita?

Licenza: MIT Piattaforme Flutter Test Dati: CC-BY

Schermata della linea 65: la mappa con le due direzioni e le fermate,
la posizione dell'utente, e l'esito Β«Adesso il percorso Γ¨ regolareΒ»


✨ In breve

  • πŸ”΄ Ti dice quali fermate saltano β€” e dove andare al loro posto. È l'informazione che serve davvero, e che GTT non pubblica quasi mai.
  • πŸ—ΊοΈ Disegna la deviazione sulla mappa, ricostruita dal testo dell'avviso: percorso normale, tratto deviato, fermate escluse.
  • 🚌 Guarda dove sono i mezzi adesso e capisce se la deviazione Γ¨ giΓ  finita β€” cosa che nessun'altra fonte sa dirti, perchΓ© GTT annuncia quando cominciano ma non quando smettono.
  • πŸ“… Separa ciΓ² che Γ¨ in corso da ciΓ² che comincerΓ . Il 19 % delle variazioni pubblicate non Γ¨ ancora in vigore.
  • ☁️ Calcola una volta per tutti. Un job su GitHub Actions legge gli avvisi ogni mezz'ora e pubblica i risultati: l'app li scarica in un secondo, senza chiavi nΓ© account. La tua posizione non lascia il dispositivo.
  • 🀐 Non inventa mai. Se non riesce a ricostruire un percorso lo dichiara e ti mostra il testo originale di GTT.

πŸ“‘ Indice

🎯 Il problema

L'avviso tipico di GTT si presenta così:

Linea 65 deviata in direzione corso Bolzano dalle 8:00 di lunedì 3 sino alle 18:00 di venerdì 7 agosto 2026. Da via Asinari di Bernezzo angolo corso Monte Grappa, per via Asinari di Bernezzo, piazza Chironi, via Medici, corso Lecce, via Lessona, segue percorso normale.

È un elenco di vie. Ma chi aspetta il bus sta a una fermata, non su una via, e per sapere se quella fermata è ancora servita dovrebbe ricostruire il percorso a mente su una mappa che non ha davanti.

L'app fa quel lavoro: geocodifica i toponimi, calcola il percorso deviato sulla rete stradale reale, lo confronta con il percorso ufficiale del GTFS e determina quali fermate restano fuori.

Note

DeviaTo β€” da Β«deviaΒ» e Β«ToΒ», che a Torino Γ¨ come si scrive Torino. Progetto personale, non affiliato a GTT nΓ© approvato da GTT.

πŸ“² Scaricala

DeviaTo Γ¨ gratuita, senza pubblicitΓ  e senza account. Arriva a breve su App Store e Google Play: i link compariranno qui e sul sito.

Aggiungi le linee che usi con +: si cercano per numero o per via. Poi apri una linea, tocca la tua fermata sulla mappa e salvala: la home ti dice subito se Γ¨ servita.

Important

Privacy. Nessun account, nessuna statistica d'uso, nessuna pubblicitΓ . La posizione, se la attivi, serve solo a mostrarti sulla mappa: non viene salvata e non esce dal telefono. Il permesso si chiede quando tocchi il pulsante, non all'apertura. Informativa completa.

🧭 Cosa fa

πŸ”΄ Fermate non servite e alternative

L'output più utile, e quello che GTT non fornisce quasi mai. Le fermate chiuse sono riassunte per tratti e non avviso per avviso: quando GTT pubblica un avviso per ogni fermata, sedici avvisi diventano «nove fermate chiuse di fila, sali a Vibò o a Statuto Nord». Le alternative privilegiano le fermate ancora servite dalla stessa linea, così da non richiedere un cambio di mezzo, ed escludono quelle chiuse da qualunque altro avviso della linea.

🌐 Mappa

Percorso normale di entrambe le direzioni con tonalitΓ  distinte, tratto deviato in rosso, fermate toccabili per il nome, fermate saltate cerchiate, e la tua posizione su richiesta. Si apre anche a tutto schermo, con una direzione sola se le due si sovrappongono e un pannello che si trascina dal basso: le fermate non servite per tratti, e quella che tocchi.

🚌 Osservazione dei mezzi in tempo reale

In tempo reale, finchΓ© non la interrompi, con i mezzi che si aggiornano sulla mappa: ognuno ha una punta verso dove sta andando e il colore della direzione che sta facendo. Continua mentre guardi altre linee β€” una alla volta, per non raddoppiare le richieste al feed di GTT. Risponde a una domanda che nessun'altra fonte copre β€” la deviazione Γ¨ giΓ  finita? β€” e a una che il testo non sa rispondere bene: dove escono e dove rientrano davvero. L'app lo dice con i nomi delle fermate, e disegna il tratto realmente percorso. È l'unico dato del sistema che non viene da un testo di GTT.

πŸ“… In corso oppure in programma

Il 19 % delle variazioni pubblicate non Γ¨ ancora in vigore. L'app le tiene separate β€” Β«comincia dopodomaniΒ» β€” invece di segnalarle come attive.

⭐ Le tue fermate

Salvi la fermata che usi β€” linea, direzione, palo β€” e la home ti risponde per quella: servita, non servita fino al…, e dove salire invece.

πŸ“„ Testo originale sempre visibile

In fondo a ogni scheda, così che il dato grezzo resti disponibile anche quando il sistema sbaglia.

🧩 Come funziona

Il passaggio da testo a geometria Γ¨ il punto in cui questi progetti si fermano. Un geocoder interrogato liberamente con Β«via RomaΒ» restituisce decine di risultati in tutto il Piemonte, e la polilinea che ne esce non ha alcun rapporto con il percorso reale della linea.

La soluzione Γ¨ il geocoding vincolato: i toponimi si cercano esclusivamente entro un chilometro dal percorso ufficiale di quella linea. È questo vincolo β€” non il modello linguistico, non l'euristica β€” a rendere affidabile l'intera catena.

πŸ“Š Le misure, sui dati reali di GTT

Grandezza Valore
Toponimi risolti correttamente 150 / 150 entro 2 km
Distanza massima di un toponimo corretto dal percorso 800 m
Distanza minima di una via estranea alla linea 1342 m
Estrazione strutturata dal testo (LLM) 34 / 34, zero toponimi inventati
Copertura della tabella alias dei nomi di linea 98,4 % (63/64)

Il margine fra 800 m e 1342 m Γ¨ ciΓ² che rende il filtro possibile: un toponimo corretto e uno estraneo si separano nettamente, e il buffer di 1 km cade in mezzo. Un buffer da 2 km lascia entrare rumore; uno da 500 m scarta vie legittime, perchΓ© una deviazione per definizione si allontana.

πŸ”— La catena

avvisi GTT (due fonti)
        β”‚
        β–Ό
  unione dei doppioni ......... 31 coppie su 189 avvisi
        β”‚
        β–Ό
  estrazione LLM .............. testo β†’ JSON (vie, direzione, tipo)
        β”‚
        β–Ό
  geocoding VINCOLATO ......... toponimi β†’ coordinate, entro 1 km dalla linea
        β”‚
        β–Ό
  routing bus (Valhalla) ...... coordinate β†’ polilinea sulla rete reale
        β”‚
        β–Ό
  cinque validazioni .......... o si dichiara l'incertezza
        β”‚
        β–Ό
  impatto sulle fermate ....... quali saltano, dove andare al loro posto

Il percorso ricostruito supera cinque validazioni prima di essere disegnato. Se una fallisce, l'app dichiara l'incertezza e mostra il testo di GTT. La regola Γ¨ esplicita:

⚠️ Meglio nessuna mappa che una mappa sbagliata. Un falso positivo fa camminare l'utente ottocento metri inutilmente, e distrugge la fiducia nello strumento.

🧱 Architettura

Il sistema Γ¨ organizzato attorno a un vincolo strutturale:

app/lib/core/ Γ¨ Dart puro. Nessun import 'package:flutter/…'.

Da questo discende il resto: la logica si esegue e si testa in millisecondi senza simulatore, si puΓ² invocare da riga di comando, e l'interfaccia Γ¨ sostituibile β€” con un'altra UI, con uno script, con un'implementazione nativa β€” senza toccare il calcolo. Se un file di core/ avesse bisogno di Flutter, quel file sarebbe nel posto sbagliato.

Il calcolo gira in un posto solo: il job su GitHub. L'app legge i risultati.

avvisi GTT ─► job su GitHub Actions (ogni 30 min) ─► gh-pages: indice.json,
                                                     percorsi/, stato/
                                                          β”‚
                                         app β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  (qualche KB)
app/tool/pubblica.dart          ← il giro del job: scarica, calcola, scrive
app/lib/
β”œβ”€β”€ core/                       ← Dart puro, zero dipendenze da Flutter
β”‚   β”œβ”€β”€ config.dart             ← tutte le soglie tarabili, in un posto solo
β”‚   β”œβ”€β”€ models/                 ← tipi di dominio, senza comportamento di rete
β”‚   β”œβ”€β”€ geo/
β”‚   β”‚   β”œβ”€β”€ projection.dart     ← gradi ↔ metri
β”‚   β”‚   β”œβ”€β”€ geometry.dart       ← distanze, proiezioni, FrΓ©chet, densify
β”‚   β”‚   └── polyline.dart       ← codifica Google polyline
β”‚   β”œβ”€β”€ gtfs/                   ← scarico e indicizzazione del GTFS statico
β”‚   β”œβ”€β”€ sources/                ← una classe per fonte GTT, intercambiabili
β”‚   β”œβ”€β”€ llm/                    ← client OpenAI-compatibile, con tetto di spesa
β”‚   β”œβ”€β”€ io/formato_pubblicato.dart ← il formato dei file fra job e app
β”‚   β”œβ”€β”€ publish/pubblicatore.dart  ← lo stato di tutte le linee, in un giro
β”‚   └── pipeline/               ← un passaggio del calcolo per file
β”‚       β”œβ”€β”€ notice_merge.dart   ← le due fonti β†’ un avviso solo
β”‚       β”œβ”€β”€ line_resolver.dart  ← Β«55Β» β†’ 55U (tabella alias)
β”‚       β”œβ”€β”€ extractor.dart      ← testo β†’ JSON strutturato (LLM)
β”‚       β”œβ”€β”€ geocoder.dart       ← toponimo β†’ coordinate, VINCOLATO
β”‚       β”œβ”€β”€ route_builder.dart  ← vie β†’ polilinea (Valhalla) + validazioni
β”‚       β”œβ”€β”€ rejoin_inference.dart ← dove rientra, quando GTT non lo dice
β”‚       β”œβ”€β”€ stop_impact.dart    ← quali fermate saltano, e le alternative
β”‚       β”œβ”€β”€ closure_summary.dart ← le fermate chiuse lette per tratti
β”‚       └── vehicle_watch.dart  ← osservazione dei mezzi in tempo reale
β”œβ”€β”€ data/                       ← dati pubblicati con copia locale, preferenze, posizione
└── ui/                         ← schermate e mappa

Perché è diviso così. Ogni file di pipeline/ è un passaggio del ragionamento ed è sostituibile isolatamente: se Photon cessa il servizio si riscrive geocoder.dart; se Valhalla pubblico sparisce, route_builder.dart; per cambiare modello linguistico, extractor.dart. Nessun altro file se ne accorge.

config.dart esiste perchΓ© quasi tutte le soglie vanno tarate sul campo. I valori marcati MISURATO vengono da rilevazioni reali, e in sette casi contraddicono le stime del progetto originale β€” fra cui la soglia di fuori-rotta (50 m misurati contro 80 stimati) e il buffer del geocoding (1 km contro 2).

πŸ”Œ Fonti dati

Fonte Uso Nota
GTFS statico GTT percorsi, fermate, orari rigenerato ogni giorno alle 04:00, CC-BY
alerts.aspx (GTFS-RT) avvisi porta il route_id canonico nel 96,7 % dei casi
/cms/variazioni (HTML) avvisi unica fonte con le date d'inizio reali; spenta nel job finchΓ© GTT non ne autorizza l'uso
vehicle_position.aspx posizioni dei mezzi si spegne di notte, il servizio no
Photon geocoding nessuna chiave richiesta
Valhalla (FOSSGIS) routing costing: bus polilinee a precisione 6
Overpass (FOSSGIS, riserva VK Maps) forma intera delle vie degli avvisi, per gli incroci (algoritmo 2) nessuna chiave; puΓ² essere lento
OpenRouter estrazione dal testo chiave del job, segreto di GitHub Actions

L'OTP di GTT β€” che il progetto originale indicava come fonte primaria β€” Γ¨ stato scartato dopo verifica: espone un build piΓΉ vecchio, i cui trip_id non esistono nel feed corrente, e a cui mancano sette linee.

🚧 Limiti noti

πŸ”• Non ti avvisa da sola: devi aprirla tu

Il job su GitHub calcola ogni mezz'ora, ma l'app non manda notifiche: i dati si vedono aprendola. Le notifiche sono il passo naturale successivo, ora che il calcolo non sta piΓΉ sul telefono.

πŸ“ Le distanze a piedi sono in linea d'aria

Quando una fermata salta, l'app ti propone quelle vicine e ti dice quanto distano — ma in linea retta, come vola un uccello, non come cammini tu. A Torino un fiume, una ferrovia o un muro possono raddoppiare il percorso reale. L'app scrive «in linea d'aria» ogni volta, così il numero non si scambia per una distanza a piedi.

πŸ”€ Usa il percorso principale della linea

La stessa linea ha spesso piΓΉ percorsi leggermente diversi: alcune corse si fermano prima, altre passano da una via invece che da un'altra. L'app usa quello piΓΉ frequente. Se una deviazione riguardasse soltanto una di quelle corse minori, l'app la calcolerebbe come se valesse per tutte.

πŸ“† Le date di inizio non sono sempre quelle vere

GTT pubblica gli avvisi in due posti, e nel feed dei dati aperti la data di inizio non Γ¨ quella vera: Γ¨ l'ora in cui l'avviso Γ¨ stato scritto (verificato su 161 avvisi su 161). Le date vere stanno nella tabella del sito di GTT, che perΓ² non Γ¨ pubblicata come dato aperto: il job non la usa finchΓ© GTT non lo autorizza. Nel frattempo una variazione annunciata in anticipo puΓ² comparire come giΓ  in corso.

πŸ§ͺ Sviluppo

Serve Flutter con Dart β‰₯ 3.12. Il codice Γ¨ lo stesso per iOS e Android: lib/core/ Γ¨ Dart puro e non sa su cosa sta girando.

cd app && flutter test      # 409 test
cd app && flutter analyze
cd app && flutter run       # legge i dati pubblicati, nessuna chiave

Per provare l'app contro un giro del job lanciato sul computer:

cd app && dart run tool/pubblica.dart --uscita ../sito/v1 --gtfs ../.gtfs
python3 -m http.server 8765 --directory ../sito &
flutter run --dart-define=DATI_URL=http://localhost:8765/v1/

Il job su GitHub

.github/workflows/pubblica.yml esegue app/tool/pubblica.dart ogni mezz'ora di giorno, ogni ora la sera e una volta di notte, e pubblica il risultato sul ramo gh-pages. A lanciarlo Γ¨ un servizio esterno (cron-job.org) che chiama workflow_dispatch: gli orari programmati di GitHub saltano troppi giri, e restano solo come riserva. Su un fork va configurato una volta:

  1. Settings β€Ί Pages: Β«Deploy from a branchΒ», ramo gh-pages, cartella /. Il ramo lo crea il primo giro.
  2. Settings β€Ί Secrets and variables β€Ί Actions: il segreto OPENROUTER_API_KEY. Senza, si pubblicano solo le fermate sospese che GTT scrive col numero, e gli altri avvisi restano da leggere.

Le richieste al modello dipendono dagli avvisi nuovi di GTT β€” una ventina al giorno, misurato il 26/09/2026 β€” e non da quante persone usano l'app: un avviso giΓ  letto e non cambiato non si rilegge.

I test sulle fonti girano offline su dati reali: il feed protobuf e la pagina HTML di GTT del 31 luglio 2026 stanno in app/test/fixtures/. I test sul GTFS si saltano da soli se i file non ci sono.

Strumenti di misura, che interrogano i servizi veri:

cd app && dart run tool/check_pipeline_live.dart     # catena completa
cd app && dart run tool/check_merge_offline.dart     # unione delle fonti

Tip

Prima di modificare qualcosa, leggi CLAUDE.md. Contiene ciΓ² che non sta nel codice: le misure e il metodo con cui sono state ottenute, il ragionamento dietro le scelte, i sette punti in cui la specifica originale Γ¨ stata smentita dai dati, e le trappole giΓ  pagate β€” per esempio il confronto per sottostringhe che faceva scattare Β«lavori stradaliΒ» sul capolinea Β«STRADA del DrossoΒ», mandando l'avviso sulla direzione sbagliata.

Due criteri per i contributi:

  1. Le soglie si tarano misurando. Gli script in scripts/ e app/tool/ esistono per questo. Un numero rimesso in discussione va rimisurato, non stimato.
  2. Dichiarare l'incertezza Γ¨ un requisito, non un ripiego. Β«Non lo soΒ» Γ¨ una risposta valida e va comunicata come tale.

Altra documentazione: app/ARCHITETTURA.md per lo stato dei moduli Β· docs/FASE-0-RISULTATI.md per le misure sulle fonti Β· docs/ per la specifica originale, che va letta sapendo che sette dei suoi assunti si sono rivelati falsi.

🀝 Da dove nasce

L'idea Γ¨ mia, e nasce da una cosa che mi capitava di continuo: gli avvisi di GTT ci sono, i dati aperti pure, le posizioni dei mezzi anche β€” ma stanno in posti diversi e in formati diversi, e nessuno li mette insieme per rispondere alla domanda che uno si fa davvero alla fermata. Volevo aggregare quelle fonti e ricavarne delle funzioni utili.

Il passaggio da quell'idea a un'app che funziona l'ho fatto insieme a Claude (Anthropic): l'architettura, il codice, le misure sulle fonti e le verifiche sul campo sono venute da quel lavoro a quattro mani. Le decisioni su cosa dovesse fare e su come dovesse comportarsi sono rimaste mie; il mestiere per realizzarle e il rigore nel misurare invece di supporre li ha messi lui.

Buona parte di quello che c'Γ¨ scritto in CLAUDE.md β€” le misure che smentiscono le stime, le trappole trovate provando l'app invece che leggendo il codice β€” Γ¨ il residuo di quel processo, ed Γ¨ la parte del progetto che credo valga di piΓΉ.

πŸ“œ Licenze e attribuzioni

🚌 Dati di trasporto GTT S.p.A. – Gruppo Torinese Trasporti, dati aperti: CC BY 4.0 sul portale del Comune, solo uso non commerciale secondo la licenza di GTT. L'attribuzione Γ¨ obbligatoria, e l'app non ha pubblicitΓ  nΓ© acquisti
πŸ—ΊοΈ Cartografia OpenStreetMap, ODbL; immagini della cartina di CARTO (Positron e Dark Matter)
🧭 Routing Valhalla ospitato da FOSSGIS
πŸ“ Geocoding Photon di Komoot
πŸ›£οΈ Vie per nome Overpass, istanze pubbliche di FOSSGIS e VK Maps, sui dati OpenStreetMap

Photon, Valhalla e Overpass sono servizi offerti gratuitamente alla comunitΓ . Li chiama solo il job, per gli avvisi nuovi: qualche decina di richieste al giorno, con pause fra le chiamate, uno User-Agent riconoscibile e l'intestazione X-Client-Id che FOSSGIS chiede alle app pubblicate.

DeviaTo non Γ¨ un'app di GTT e non Γ¨ collegata a GTT.

Il codice Γ¨ distribuito con licenza MIT β€” vedi LICENSE. La licenza riguarda il codice, non i dati.

About

Trasforma gli avvisi di deviazione di GTT Torino in mappe: quali fermate saltano e dove andare al loro posto.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages