T19/T21: map and export, completing Phase 4

flutter_map with the same OSM tiles and no-API-key reasoning that chose osmdroid.
All four load-bearing behaviours ported and, unlike the native app's map, tested:
one polyline per segment so a pause is a real gap, render-only decimation, the
zoom clamp at OSM's max tile zoom with a short-ride fallback, and a real user
agent. Speed colouring is bucketed per run rather than per-vertex, since neither
osmdroid nor flutter_map makes per-vertex paint reasonable.

Export via share_plus, which also handles the iPad popover anchor a naive port
forgets. It passes the raw stored points, never the map's decimated path.

164 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 16:27:42 -05:00
parent bcc12b314f
commit d501528e69
20 changed files with 1085 additions and 478 deletions

View File

@@ -55,10 +55,10 @@ class RecordingEngine {
required LocationSource locationSource,
LiveTelemetry? liveTelemetry,
int Function()? clock,
}) : _repo = repository,
_source = locationSource,
_live = liveTelemetry ?? LiveTelemetry.instance,
_now = clock ?? (() => DateTime.now().millisecondsSinceEpoch);
}) : _repo = repository,
_source = locationSource,
_live = liveTelemetry ?? LiveTelemetry.instance,
_now = clock ?? (() => DateTime.now().millisecondsSinceEpoch);
final TripRepository _repo;
final LocationSource _source;
@@ -265,20 +265,20 @@ class RecordingEngine {
/// One flush cycle: take up to [flushSize] buffered points and write them.
Future<void> _flush() => _writeLock.synchronized(() async {
if (_pending.isEmpty) return;
final take = _pending.length < flushSize ? _pending.length : flushSize;
final batch = _pending.sublist(0, take);
_pending.removeRange(0, take);
await _persist(batch);
});
if (_pending.isEmpty) return;
final take = _pending.length < flushSize ? _pending.length : flushSize;
final batch = _pending.sublist(0, take);
_pending.removeRange(0, take);
await _persist(batch);
});
/// Writes everything currently queued and returns once it has landed.
Future<void> _drain() => _writeLock.synchronized(() async {
if (_pending.isEmpty) return;
final batch = List<TrackPoint>.from(_pending);
_pending.clear();
await _persist(batch);
});
if (_pending.isEmpty) return;
final batch = List<TrackPoint>.from(_pending);
_pending.clear();
await _persist(batch);
});
Future<void> _persist(List<TrackPoint> points) async {
if (points.isEmpty) return;