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-03 14:33:26 -07:00
parent be446fbcbc
commit 2d92214bed
36 changed files with 3940 additions and 351 deletions
+12 -1
View File
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import '../services/service_policy.dart';
/// A specific tile type within a provider
class TileType {
final String id;
@@ -10,7 +12,7 @@ class TileType {
final Uint8List? previewTile; // Single tile image data for preview
final int maxZoom; // Maximum zoom level for this tile type
const TileType({
TileType({
required this.id,
required this.name,
required this.urlTemplate,
@@ -76,6 +78,15 @@ class TileType {
/// Check if this tile type needs an API key
bool get requiresApiKey => urlTemplate.contains('{api_key}');
/// The service policy that applies to this tile type's server.
/// Cached because [urlTemplate] is immutable.
late final ServicePolicy servicePolicy =
ServicePolicyResolver.resolve(urlTemplate);
/// Whether this tile server's usage policy permits offline/bulk downloading.
/// Resolved via [ServicePolicyResolver] from the URL template.
bool get allowsOfflineDownload => servicePolicy.allowsOfflineDownload;
Map<String, dynamic> toJson() => {
'id': id,
'name': name,