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:
@@ -68,15 +68,14 @@ class GeolocatorLocationSource implements LocationSource {
|
||||
|
||||
await _ensurePermission();
|
||||
|
||||
_subscription = Geolocator.getPositionStream(
|
||||
locationSettings: _settings(),
|
||||
).listen(
|
||||
(p) => _controller.add(_toFix(p)),
|
||||
// A transient platform error must not close the stream: the engine treats a closed
|
||||
// stream as "recording stopped", which is a user-visible event.
|
||||
onError: (Object _) {},
|
||||
cancelOnError: false,
|
||||
);
|
||||
_subscription = Geolocator.getPositionStream(locationSettings: _settings())
|
||||
.listen(
|
||||
(p) => _controller.add(_toFix(p)),
|
||||
// A transient platform error must not close the stream: the engine treats a closed
|
||||
// stream as "recording stopped", which is a user-visible event.
|
||||
onError: (Object _) {},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -153,12 +152,12 @@ class GeolocatorLocationSource implements LocationSource {
|
||||
}
|
||||
|
||||
LocationFix _toFix(Position p) => LocationFix(
|
||||
timestamp: p.timestamp.millisecondsSinceEpoch,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
speedMps: p.speed,
|
||||
altitudeM: p.altitude,
|
||||
accuracyM: p.accuracy,
|
||||
bearingDeg: p.heading,
|
||||
);
|
||||
timestamp: p.timestamp.millisecondsSinceEpoch,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
speedMps: p.speed,
|
||||
altitudeM: p.altitude,
|
||||
accuracyM: p.accuracy,
|
||||
bearingDeg: p.heading,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ class LocationException implements Exception {
|
||||
final String? message;
|
||||
|
||||
@override
|
||||
String toString() => 'LocationException($failure${message == null ? '' : ': $message'})';
|
||||
String toString() =>
|
||||
'LocationException($failure${message == null ? '' : ': $message'})';
|
||||
}
|
||||
|
||||
/// A source of GPS fixes.
|
||||
@@ -154,14 +155,15 @@ class FakeLocationSource implements LocationSource {
|
||||
double altitudeM = 1000.0,
|
||||
double accuracyM = 5.0,
|
||||
double bearingDeg = 0.0,
|
||||
}) =>
|
||||
emit(LocationFix(
|
||||
timestamp: timestamp,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
speedMps: speedMps,
|
||||
altitudeM: altitudeM,
|
||||
accuracyM: accuracyM,
|
||||
bearingDeg: bearingDeg,
|
||||
));
|
||||
}) => emit(
|
||||
LocationFix(
|
||||
timestamp: timestamp,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
speedMps: speedMps,
|
||||
altitudeM: altitudeM,
|
||||
accuracyM: accuracyM,
|
||||
bearingDeg: bearingDeg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user