Add offline-first tile system with per-provider caching and error retry

- Add ServicePolicy framework with OSM-specific rate limiting and TTL
- Add per-provider disk tile cache (ProviderTileCacheStore) with O(1)
  lookup, oldest-modified eviction, and ETag/304 revalidation
- Rewrite DeflockTileProvider with two paths: common (NetworkTileProvider)
  and offline-first (disk cache -> local tiles -> network with caching)
- Add zoom-aware offline routing so tiles outside offline area zoom ranges
  use the efficient common path instead of the overhead-heavy offline path
- Fix HTTP client lifecycle: dispose() is now a no-op for flutter_map
  widget recycling; shutdown() handles permanent teardown
- Add TileLayerManager with exponential backoff retry (2s->60s cap),
  provider switch detection, and backoff reset
- Guard null provider/tileType in download dialog with localized error
- Fix Nominatim cache key to use normalized viewbox values
- Comprehensive test coverage (1800+ lines across 6 test files)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Doug Borg
2026-03-07 12:34:01 -07:00
co-authored by Claude Opus 4.6
parent be446fbcbc
commit 2d92214bed
36 changed files with 3940 additions and 351 deletions
+13 -7
View File
@@ -181,7 +181,10 @@
"offlineModeWarning": "Download disabilitati in modalità offline. Disabilita la modalità offline per scaricare nuove aree.",
"areaTooBigMessage": "Ingrandisci almeno al livello {} per scaricare aree offline. I download di aree grandi possono rendere l'app non reattiva.",
"downloadStarted": "Download avviato! Recupero tile e nodi...",
"downloadFailed": "Impossibile avviare il download: {}"
"downloadFailed": "Impossibile avviare il download: {}",
"offlineNotPermitted": "Il server {} non consente i download offline. Passa a un fornitore di tile che consenta l'uso offline (ad es., Bing Maps, Mapbox o un server di tile auto-ospitato).",
"currentTileProvider": "tile attuale",
"noTileProviderSelected": "Nessun provider di tile selezionato. Seleziona uno stile di mappa prima di scaricare un'area offline."
},
"downloadStarted": {
"title": "Download Avviato",
@@ -329,13 +332,16 @@
"addProfileChoiceMessage": "Come desideri aggiungere un profilo?",
"createCustomProfile": "Crea Profilo Personalizzato",
"createCustomProfileDescription": "Crea un profilo da zero con i tuoi tag",
"importFromWebsite": "Importa da Sito Web",
"importFromWebsite": "Importa da Sito Web",
"importFromWebsiteDescription": "Sfoglia e importa profili da deflock.me/identify"
},
"mapTiles": {
"title": "Tile Mappa",
"manageProviders": "Gestisci Fornitori",
"attribution": "Attribuzione Mappa"
"attribution": "Attribuzione Mappa",
"mapAttribution": "Attribuzione mappa: {}",
"couldNotOpenLink": "Impossibile aprire il link",
"openLicense": "Apri licenza: {}"
},
"profileEditor": {
"viewProfile": "Visualizza Profilo",
@@ -362,7 +368,7 @@
},
"operatorProfileEditor": {
"newOperatorProfile": "Nuovo Profilo Operatore",
"editOperatorProfile": "Modifica Profilo Operatore",
"editOperatorProfile": "Modifica Profilo Operatore",
"operatorName": "Nome operatore",
"operatorNameHint": "es., Dipartimento di Polizia di Austin",
"operatorNameRequired": "Il nome dell'operatore è obbligatorio",
@@ -520,7 +526,7 @@
"updateFailed": "Aggiornamento posizioni sospette fallito",
"neverFetched": "Mai recuperato",
"daysAgo": "{} giorni fa",
"hoursAgo": "{} ore fa",
"hoursAgo": "{} ore fa",
"minutesAgo": "{} minuti fa",
"justNow": "Proprio ora"
},
@@ -528,7 +534,7 @@
"title": "Posizione Sospetta #{}",
"ticketNo": "N. Ticket",
"address": "Indirizzo",
"street": "Via",
"street": "Via",
"city": "Città",
"state": "Stato",
"intersectingStreet": "Via che Interseca",
@@ -552,4 +558,4 @@
"metricDescription": "Metrico (km, m)",
"imperialDescription": "Imperiale (mi, ft)"
}
}
}