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": "Téléchargements désactivés en mode hors ligne. Désactivez le mode hors ligne pour télécharger de nouvelles zones.",
"areaTooBigMessage": "Zoomez au moins au niveau {} pour télécharger des zones hors ligne. Les téléchargements de grandes zones peuvent rendre l'application non réactive.",
"downloadStarted": "Téléchargement démarré ! Récupération des tuiles et nœuds...",
"downloadFailed": "Échec du démarrage du téléchargement: {}"
"downloadFailed": "Échec du démarrage du téléchargement: {}",
"offlineNotPermitted": "Le serveur {} ne permet pas les téléchargements hors ligne. Passez à un fournisseur de tuiles qui autorise l'utilisation hors ligne (par ex., Bing Maps, Mapbox ou un serveur de tuiles auto-hébergé).",
"currentTileProvider": "tuile actuelle",
"noTileProviderSelected": "Aucun fournisseur de tuiles sélectionné. Veuillez choisir un style de carte avant de télécharger une zone hors ligne."
},
"downloadStarted": {
"title": "Téléchargement Démarré",
@@ -329,13 +332,16 @@
"addProfileChoiceMessage": "Comment souhaitez-vous ajouter un profil?",
"createCustomProfile": "Créer Profil Personnalisé",
"createCustomProfileDescription": "Créer un profil à partir de zéro avec vos propres balises",
"importFromWebsite": "Importer depuis Site Web",
"importFromWebsite": "Importer depuis Site Web",
"importFromWebsiteDescription": "Parcourir et importer des profils depuis deflock.me/identify"
},
"mapTiles": {
"title": "Tuiles de Carte",
"manageProviders": "Gérer Fournisseurs",
"attribution": "Attribution de Carte"
"attribution": "Attribution de Carte",
"mapAttribution": "Attribution de la carte : {}",
"couldNotOpenLink": "Impossible d'ouvrir le lien",
"openLicense": "Ouvrir la licence : {}"
},
"profileEditor": {
"viewProfile": "Voir Profil",
@@ -362,7 +368,7 @@
},
"operatorProfileEditor": {
"newOperatorProfile": "Nouveau Profil d'Opérateur",
"editOperatorProfile": "Modifier Profil d'Opérateur",
"editOperatorProfile": "Modifier Profil d'Opérateur",
"operatorName": "Nom de l'opérateur",
"operatorNameHint": "ex., Département de Police d'Austin",
"operatorNameRequired": "Le nom de l'opérateur est requis",
@@ -520,7 +526,7 @@
"updateFailed": "Échec de la mise à jour des emplacements suspects",
"neverFetched": "Jamais récupéré",
"daysAgo": "il y a {} jours",
"hoursAgo": "il y a {} heures",
"hoursAgo": "il y a {} heures",
"minutesAgo": "il y a {} minutes",
"justNow": "À l'instant"
},
@@ -528,7 +534,7 @@
"title": "Emplacement Suspect #{}",
"ticketNo": "N° de Ticket",
"address": "Adresse",
"street": "Rue",
"street": "Rue",
"city": "Ville",
"state": "État",
"intersectingStreet": "Rue Transversale",
@@ -552,4 +558,4 @@
"metricDescription": "Métrique (km, m)",
"imperialDescription": "Impérial (mi, ft)"
}
}
}