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
+4 -9
View File
@@ -284,17 +284,12 @@ class MapViewState extends State<MapView> {
onProfilesChanged: _refreshNodesFromProvider,
);
// Check if tile type OR offline mode changed and clear cache if needed
final cacheCleared = _tileManager.checkAndClearCacheIfNeeded(
// Check if provider, tile type, or offline mode changed and clear cache if needed
_tileManager.checkAndClearCacheIfNeeded(
currentProviderId: appState.selectedTileProvider?.id,
currentTileTypeId: appState.selectedTileType?.id,
currentOfflineMode: appState.offlineMode,
);
if (cacheCleared) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_tileManager.clearTileQueue();
});
}
// Seed addmode target once, after first controller center is available.
if (session != null && session.target == null) {
@@ -396,7 +391,7 @@ class MapViewState extends State<MapView> {
if (_activePointers > 0) _activePointers--;
},
child: FlutterMap(
key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_${_tileManager.mapRebuildKey}'),
key: ValueKey('map_${appState.selectedTileProvider?.id ?? 'none'}_${appState.selectedTileType?.id ?? 'none'}_${appState.offlineMode}_${_tileManager.mapRebuildKey}'),
mapController: _controller.mapController,
options: MapOptions(
initialCenter: _gpsController.currentLocation ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194),