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:
2026-08-17 19:45:40 -05:00
parent be50448917
commit e4ffafe9e0
16 changed files with 1089 additions and 5 deletions

View File

@@ -44,12 +44,18 @@ class RideMap extends StatefulWidget {
required this.segments,
this.height = 320,
this.follow = false,
this.tileProvider,
});
final List<TrackPoint> points;
final List<Segment> segments;
final double height;
/// V3-11: when supplied, tiles are read from (and written through to) the offline
/// tile cache instead of flutter_map's own uncapped default. Null falls back to
/// ordinary networked tiles -- used whenever the cache isn't ready yet, or in tests.
final TileProvider? tileProvider;
/// V3-04: keep the latest point centred while recording. A manual pan/pinch turns
/// this off until the widget is rebuilt fresh (e.g. a new ride) — chasing the rider
/// back to centre after they deliberately looked elsewhere would be worse than not
@@ -184,6 +190,7 @@ class _RideMapState extends State<RideMap> with WidgetsBindingObserver {
maxNativeZoom: maxTileZoom.toInt(),
// Respect OSM's usage policy: render what is looked at, never bulk prefetch.
panBuffer: 0,
tileProvider: widget.tileProvider,
),
PolylineLayer(polylines: polylines),
],