/// Feeds `RideMap`'s `TileLayer` from [TileCache] first, network second -- the same /// cache a V3-11 pre-download populates, so a rider who downloaded a corridor actually /// sees it render without a request going out. See V3-11. library; import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart' as http; import 'map_connectivity.dart'; import 'tile_cache.dart'; import 'tile_math.dart'; class CachedTileProvider extends TileProvider { CachedTileProvider({required this.cache, this.connectivity, http.Client? client}) : _client = client ?? http.Client(), super(); final TileCache cache; final http.Client _client; /// UI-02: told about every fetch outcome so it can decide whether the map should be /// showing an animated skeleton instead of tiles. Null in tests/callers that don't /// care -- skeleton mode is a UI concern layered on top of caching, not something /// this provider requires to function. final MapConnectivityState? connectivity; @override ImageProvider getImage(TileCoordinates coordinates, TileLayer options) => _CacheBackedImage( key: TileKey(coordinates.z, coordinates.x, coordinates.y), url: getTileUrl(coordinates, options), headers: headers, cache: cache, client: _client, connectivity: connectivity, ); } class _CacheBackedImage extends ImageProvider<_CacheBackedImage> { const _CacheBackedImage({ required this.key, required this.url, required this.headers, required this.cache, required this.client, this.connectivity, }); final TileKey key; final String url; final Map headers; final TileCache cache; final http.Client client; final MapConnectivityState? connectivity; @override Future<_CacheBackedImage> obtainKey(ImageConfiguration configuration) => SynchronousFuture(this); @override ImageStreamCompleter loadImage(_CacheBackedImage key, ImageDecoderCallback decode) => MultiFrameImageStreamCompleter( codec: _load(decode), scale: 1.0, debugLabel: url, ); Future _load(ImageDecoderCallback decode) async { final cached = await cache.get(key); // A cache hit says nothing about current connectivity either way -- it's not a // network round trip, so it neither counts as a success nor resets a failure run. final bytes = cached ?? await _fetchAndStore(); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); return decode(buffer); } Future _fetchAndStore() async { try { final response = await client.get(Uri.parse(url), headers: headers); if (response.statusCode != 200) { throw Exception('Tile fetch failed: ${response.statusCode} for $url'); } final bytes = response.bodyBytes; // Write-through: viewing a tile online caches it for later, exactly like // flutter_map's own default caching did -- just capped and evictable now. await cache.put(key, bytes); connectivity?.reportSuccess(); return bytes; } catch (_) { // UI-02: a cache miss whose network fetch also failed is exactly the "no // connection" signal skeleton mode is watching for -- report it and rethrow so // flutter_map's own error handling for this tile is unchanged. connectivity?.reportFailure(); rethrow; } } @override bool operator ==(Object other) => other is _CacheBackedImage && other.key == key; @override int get hashCode => key.hashCode; }