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
@@ -144,7 +144,10 @@
"offlineModeWarning": "Downloads im Offline-Modus deaktiviert. Deaktivieren Sie den Offline-Modus, um neue Bereiche herunterzuladen.",
"areaTooBigMessage": "Zoomen Sie auf mindestens Stufe {} heran, um Offline-Bereiche herunterzuladen. Downloads großer Gebiete können die App zum Absturz bringen.",
"downloadStarted": "Download gestartet! Lade Kacheln und Knoten...",
"downloadFailed": "Download konnte nicht gestartet werden: {}"
"downloadFailed": "Download konnte nicht gestartet werden: {}",
"offlineNotPermitted": "Der {}-Server erlaubt keine Offline-Downloads. Wechseln Sie zu einem Kachelanbieter, der Offline-Nutzung unterstützt (z. B. Bing Maps, Mapbox oder ein selbst gehosteter Kachelserver).",
"currentTileProvider": "aktuelle Kachel",
"noTileProviderSelected": "Kein Kachelanbieter ausgewählt. Bitte wählen Sie einen Kartenstil, bevor Sie einen Offlinebereich herunterladen."
},
"downloadStarted": {
"title": "Download gestartet",
@@ -292,13 +295,16 @@
"addProfileChoiceMessage": "Wie möchten Sie ein Profil hinzufügen?",
"createCustomProfile": "Benutzerdefiniertes Profil Erstellen",
"createCustomProfileDescription": "Erstellen Sie ein Profil von Grund auf mit Ihren eigenen Tags",
"importFromWebsite": "Von Webseite Importieren",
"importFromWebsite": "Von Webseite Importieren",
"importFromWebsiteDescription": "Profile von deflock.me/identify durchsuchen und importieren"
},
"mapTiles": {
"title": "Karten-Kacheln",
"manageProviders": "Anbieter Verwalten",
"attribution": "Karten-Zuschreibung"
"attribution": "Karten-Zuschreibung",
"mapAttribution": "Kartenquelle: {}",
"couldNotOpenLink": "Link konnte nicht geöffnet werden",
"openLicense": "Lizenz öffnen: {}"
},
"profileEditor": {
"viewProfile": "Profil Anzeigen",
@@ -325,7 +331,7 @@
},
"operatorProfileEditor": {
"newOperatorProfile": "Neues Betreiber-Profil",
"editOperatorProfile": "Betreiber-Profil Bearbeiten",
"editOperatorProfile": "Betreiber-Profil Bearbeiten",
"operatorName": "Betreiber-Name",
"operatorNameHint": "z.B. Polizei Austin",
"operatorNameRequired": "Betreiber-Name ist erforderlich",
@@ -520,7 +526,7 @@
"updateFailed": "Aktualisierung der verdächtigen Standorte fehlgeschlagen",
"neverFetched": "Nie abgerufen",
"daysAgo": "vor {} Tagen",
"hoursAgo": "vor {} Stunden",
"hoursAgo": "vor {} Stunden",
"minutesAgo": "vor {} Minuten",
"justNow": "Gerade eben"
},
@@ -528,7 +534,7 @@
"title": "Verdächtiger Standort #{}",
"ticketNo": "Ticket-Nr.",
"address": "Adresse",
"street": "Straße",
"street": "Straße",
"city": "Stadt",
"state": "Bundesland",
"intersectingStreet": "Kreuzende Straße",
@@ -552,4 +558,4 @@
"metricDescription": "Metrisch (km, m)",
"imperialDescription": "Imperial (mi, ft)"
}
}
}