V3-11: offline tile pre-download along a route corridor
Three pure modules: tile_math.dart (tilesForBounds/tilesAlongRoute, hard cap enforced by throwing), tile_cache.dart (FileTileCache with LRU eviction before any write that would exceed the cap), tile_downloader.dart (sequential, rate-limited, cooperatively cancellable, one bad tile doesn't abort the rest). CachedTileProvider wires the cache into flutter_map via a custom ImageProvider and now backs RideMap and RoutePlannerScreen's tile layers, so ordinary viewing write-throughs into the same capped cache. RoutePlannerScreen gained a route-corridor download action (no rectangle-selection UI -- the ticket names the corridor as strictly better and V3-07 already exists to hang it off of), with a count/size confirmation before any request and a cancellable progress dialog. Fixed a real bug before shipping: a StatefulBuilder-based progress dialog would have started a new overlapping download subscription on every single progress tick; moved to a dedicated StatefulWidget that subscribes once in initState. Marked partially done: aeroplane-mode verification on a real device is the ticket's own acceptance criterion and needs a phone this environment doesn't have.
This commit is contained in:
87
lib/src/tiles/cached_tile_provider.dart
Normal file
87
lib/src/tiles/cached_tile_provider.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
/// 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 'tile_cache.dart';
|
||||
import 'tile_math.dart';
|
||||
|
||||
class CachedTileProvider extends TileProvider {
|
||||
CachedTileProvider({required this.cache, http.Client? client})
|
||||
: _client = client ?? http.Client(),
|
||||
super();
|
||||
|
||||
final TileCache cache;
|
||||
final http.Client _client;
|
||||
|
||||
@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,
|
||||
);
|
||||
}
|
||||
|
||||
class _CacheBackedImage extends ImageProvider<_CacheBackedImage> {
|
||||
const _CacheBackedImage({
|
||||
required this.key,
|
||||
required this.url,
|
||||
required this.headers,
|
||||
required this.cache,
|
||||
required this.client,
|
||||
});
|
||||
|
||||
final TileKey key;
|
||||
final String url;
|
||||
final Map<String, String> headers;
|
||||
final TileCache cache;
|
||||
final http.Client client;
|
||||
|
||||
@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<ui.Codec> _load(ImageDecoderCallback decode) async {
|
||||
final cached = await cache.get(key);
|
||||
final bytes = cached ?? await _fetchAndStore();
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
return decode(buffer);
|
||||
}
|
||||
|
||||
Future<Uint8List> _fetchAndStore() async {
|
||||
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);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is _CacheBackedImage && other.key == key;
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode;
|
||||
}
|
||||
Reference in New Issue
Block a user