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:
@@ -105,82 +105,89 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) => m.createAll(),
|
||||
beforeOpen: (details) async {
|
||||
// Non-negotiable: without this the CASCADE relationships above do nothing.
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
// A ride is unrecoverable if a write is lost to a crash mid-flush, but full
|
||||
// sync on every insert at 2 Hz burns battery. WAL with NORMAL sync is the
|
||||
// standard compromise and survives app crashes; only an OS-level crash can
|
||||
// lose the last few points.
|
||||
await customStatement('PRAGMA journal_mode = WAL');
|
||||
await customStatement('PRAGMA synchronous = NORMAL');
|
||||
},
|
||||
);
|
||||
onCreate: (m) => m.createAll(),
|
||||
beforeOpen: (details) async {
|
||||
// Non-negotiable: without this the CASCADE relationships above do nothing.
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
// A ride is unrecoverable if a write is lost to a crash mid-flush, but full
|
||||
// sync on every insert at 2 Hz burns battery. WAL with NORMAL sync is the
|
||||
// standard compromise and survives app crashes; only an OS-level crash can
|
||||
// lose the last few points.
|
||||
await customStatement('PRAGMA journal_mode = WAL');
|
||||
await customStatement('PRAGMA synchronous = NORMAL');
|
||||
},
|
||||
);
|
||||
|
||||
// --- Trips ---------------------------------------------------------------
|
||||
|
||||
/// The in-progress ride, or null. This is the source of truth for "are we recording" —
|
||||
/// it survives process death, which an in-memory flag cannot.
|
||||
Stream<domain.Trip?> watchActiveTrip() => (select(trips)
|
||||
..where((t) => t.endedAt.isNull())
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(1))
|
||||
.watchSingleOrNull()
|
||||
.map((r) => r == null ? null : _toTrip(r));
|
||||
|
||||
Future<domain.Trip?> getActiveTrip() async {
|
||||
final row = await (select(trips)
|
||||
..where((t) => t.endedAt.isNull())
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toTrip(row);
|
||||
}
|
||||
|
||||
Stream<List<domain.Trip>> watchCompletedTrips() => (select(trips)
|
||||
..where((t) => t.endedAt.isNotNull())
|
||||
..orderBy([(t) => OrderingTerm.desc(t.startedAt)]))
|
||||
.watch()
|
||||
.map((rows) => rows.map(_toTrip).toList());
|
||||
|
||||
Stream<domain.Trip?> watchTrip(int id) =>
|
||||
(select(trips)..where((t) => t.id.equals(id)))
|
||||
Stream<domain.Trip?> watchActiveTrip() =>
|
||||
(select(trips)
|
||||
..where((t) => t.endedAt.isNull())
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(1))
|
||||
.watchSingleOrNull()
|
||||
.map((r) => r == null ? null : _toTrip(r));
|
||||
|
||||
Future<domain.Trip?> getTrip(int id) async {
|
||||
Future<domain.Trip?> getActiveTrip() async {
|
||||
final row =
|
||||
await (select(trips)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
await (select(trips)
|
||||
..where((t) => t.endedAt.isNull())
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toTrip(row);
|
||||
}
|
||||
|
||||
Stream<List<domain.Trip>> watchCompletedTrips() =>
|
||||
(select(trips)
|
||||
..where((t) => t.endedAt.isNotNull())
|
||||
..orderBy([(t) => OrderingTerm.desc(t.startedAt)]))
|
||||
.watch()
|
||||
.map((rows) => rows.map(_toTrip).toList());
|
||||
|
||||
Stream<domain.Trip?> watchTrip(int id) =>
|
||||
(select(trips)..where((t) => t.id.equals(id))).watchSingleOrNull().map(
|
||||
(r) => r == null ? null : _toTrip(r),
|
||||
);
|
||||
|
||||
Future<domain.Trip?> getTrip(int id) async {
|
||||
final row = await (select(
|
||||
trips,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
return row == null ? null : _toTrip(row);
|
||||
}
|
||||
|
||||
Future<int> insertTrip(domain.Trip trip) => into(trips).insert(
|
||||
TripsCompanion.insert(
|
||||
startedAt: trip.startedAt,
|
||||
endedAt: Value(trip.endedAt),
|
||||
name: Value(trip.name),
|
||||
state: trip.state,
|
||||
distanceM: Value(trip.distanceM),
|
||||
movingMillis: Value(trip.movingMillis),
|
||||
maxSpeedKmh: Value(trip.maxSpeedKmh),
|
||||
elevationGainM: Value(trip.elevationGainM),
|
||||
pointCount: Value(trip.pointCount),
|
||||
),
|
||||
);
|
||||
TripsCompanion.insert(
|
||||
startedAt: trip.startedAt,
|
||||
endedAt: Value(trip.endedAt),
|
||||
name: Value(trip.name),
|
||||
state: trip.state,
|
||||
distanceM: Value(trip.distanceM),
|
||||
movingMillis: Value(trip.movingMillis),
|
||||
maxSpeedKmh: Value(trip.maxSpeedKmh),
|
||||
elevationGainM: Value(trip.elevationGainM),
|
||||
pointCount: Value(trip.pointCount),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> renameTrip(int id, String? name) =>
|
||||
(update(trips)..where((t) => t.id.equals(id)))
|
||||
.write(TripsCompanion(name: Value(name)));
|
||||
Future<void> renameTrip(int id, String? name) => (update(
|
||||
trips,
|
||||
)..where((t) => t.id.equals(id))).write(TripsCompanion(name: Value(name)));
|
||||
|
||||
Future<void> setTripState(int id, domain.TripState state) =>
|
||||
(update(trips)..where((t) => t.id.equals(id)))
|
||||
.write(TripsCompanion(state: Value(state)));
|
||||
Future<void> setTripState(int id, domain.TripState state) => (update(
|
||||
trips,
|
||||
)..where((t) => t.id.equals(id))).write(TripsCompanion(state: Value(state)));
|
||||
|
||||
Future<void> closeTrip(int id, int endedAt,
|
||||
{domain.TripState state = domain.TripState.completed}) =>
|
||||
(update(trips)..where((t) => t.id.equals(id))).write(
|
||||
TripsCompanion(endedAt: Value(endedAt), state: Value(state)));
|
||||
Future<void> closeTrip(
|
||||
int id,
|
||||
int endedAt, {
|
||||
domain.TripState state = domain.TripState.completed,
|
||||
}) => (update(trips)..where((t) => t.id.equals(id))).write(
|
||||
TripsCompanion(endedAt: Value(endedAt), state: Value(state)),
|
||||
);
|
||||
|
||||
/// Persists the running totals. Called once per writer flush (~every 2 s), so it stays
|
||||
/// a narrow targeted update rather than a full row rewrite.
|
||||
@@ -191,62 +198,64 @@ class AppDatabase extends _$AppDatabase {
|
||||
required double maxSpeedKmh,
|
||||
required double elevationGainM,
|
||||
required int pointCount,
|
||||
}) =>
|
||||
(update(trips)..where((t) => t.id.equals(id))).write(TripsCompanion(
|
||||
distanceM: Value(distanceM),
|
||||
movingMillis: Value(movingMillis),
|
||||
maxSpeedKmh: Value(maxSpeedKmh),
|
||||
elevationGainM: Value(elevationGainM),
|
||||
pointCount: Value(pointCount),
|
||||
));
|
||||
}) => (update(trips)..where((t) => t.id.equals(id))).write(
|
||||
TripsCompanion(
|
||||
distanceM: Value(distanceM),
|
||||
movingMillis: Value(movingMillis),
|
||||
maxSpeedKmh: Value(maxSpeedKmh),
|
||||
elevationGainM: Value(elevationGainM),
|
||||
pointCount: Value(pointCount),
|
||||
),
|
||||
);
|
||||
|
||||
/// Segments and points go with it via CASCADE.
|
||||
Future<void> deleteTrip(int id) =>
|
||||
(delete(trips)..where((t) => t.id.equals(id))).go();
|
||||
|
||||
Future<int> countTrips() async =>
|
||||
(await select(trips).get()).length;
|
||||
Future<int> countTrips() async => (await select(trips).get()).length;
|
||||
|
||||
/// Test/maintenance helper. Segments and points follow via CASCADE.
|
||||
Future<void> deleteAllTrips() => delete(trips).go();
|
||||
|
||||
// --- Segments ------------------------------------------------------------
|
||||
|
||||
Future<int> insertSegment(int tripId, int startedAt) =>
|
||||
into(segments).insert(
|
||||
SegmentsCompanion.insert(tripId: tripId, startedAt: startedAt),
|
||||
);
|
||||
Future<int> insertSegment(int tripId, int startedAt) => into(segments)
|
||||
.insert(SegmentsCompanion.insert(tripId: tripId, startedAt: startedAt));
|
||||
|
||||
Future<List<domain.Segment>> segmentsForTrip(int tripId) async {
|
||||
final rows = await (select(segments)
|
||||
..where((s) => s.tripId.equals(tripId))
|
||||
..orderBy([(s) => OrderingTerm.asc(s.id)]))
|
||||
.get();
|
||||
final rows =
|
||||
await (select(segments)
|
||||
..where((s) => s.tripId.equals(tripId))
|
||||
..orderBy([(s) => OrderingTerm.asc(s.id)]))
|
||||
.get();
|
||||
return rows.map(_toSegment).toList();
|
||||
}
|
||||
|
||||
/// The segment currently being recorded into, if any.
|
||||
Future<domain.Segment?> openSegment(int tripId) async {
|
||||
final row = await (select(segments)
|
||||
..where((s) => s.tripId.equals(tripId) & s.endedAt.isNull())
|
||||
..orderBy([(s) => OrderingTerm.desc(s.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
final row =
|
||||
await (select(segments)
|
||||
..where((s) => s.tripId.equals(tripId) & s.endedAt.isNull())
|
||||
..orderBy([(s) => OrderingTerm.desc(s.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toSegment(row);
|
||||
}
|
||||
|
||||
Future<void> closeSegment(int id, int endedAt) =>
|
||||
(update(segments)..where((s) => s.id.equals(id)))
|
||||
.write(SegmentsCompanion(endedAt: Value(endedAt)));
|
||||
(update(segments)..where((s) => s.id.equals(id))).write(
|
||||
SegmentsCompanion(endedAt: Value(endedAt)),
|
||||
);
|
||||
|
||||
/// Used by merge: re-parents a trip's segments onto the surviving trip.
|
||||
Future<void> reparentSegments(int oldTripId, int newTripId) =>
|
||||
(update(segments)..where((s) => s.tripId.equals(oldTripId)))
|
||||
.write(SegmentsCompanion(tripId: Value(newTripId)));
|
||||
(update(segments)..where((s) => s.tripId.equals(oldTripId))).write(
|
||||
SegmentsCompanion(tripId: Value(newTripId)),
|
||||
);
|
||||
|
||||
Future<int> countSegmentsForTrip(int tripId) async =>
|
||||
(await (select(segments)..where((s) => s.tripId.equals(tripId))).get())
|
||||
.length;
|
||||
Future<int> countSegmentsForTrip(int tripId) async => (await (select(
|
||||
segments,
|
||||
)..where((s) => s.tripId.equals(tripId))).get()).length;
|
||||
|
||||
// --- Track points --------------------------------------------------------
|
||||
|
||||
@@ -256,71 +265,77 @@ class AppDatabase extends _$AppDatabase {
|
||||
await batch((b) {
|
||||
b.insertAll(
|
||||
trackPoints,
|
||||
points.map((p) => TrackPointsCompanion.insert(
|
||||
tripId: p.tripId,
|
||||
segmentId: p.segmentId,
|
||||
timestamp: p.timestamp,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
speedKmh: p.speedKmh,
|
||||
altitudeM: p.altitudeM,
|
||||
accuracyM: Value(p.accuracyM),
|
||||
bearingDeg: Value(p.bearingDeg),
|
||||
synced: Value(p.synced),
|
||||
)),
|
||||
points.map(
|
||||
(p) => TrackPointsCompanion.insert(
|
||||
tripId: p.tripId,
|
||||
segmentId: p.segmentId,
|
||||
timestamp: p.timestamp,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
speedKmh: p.speedKmh,
|
||||
altitudeM: p.altitudeM,
|
||||
accuracyM: Value(p.accuracyM),
|
||||
bearingDeg: Value(p.bearingDeg),
|
||||
synced: Value(p.synced),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> insertPoint(domain.TrackPoint p) =>
|
||||
into(trackPoints).insert(TrackPointsCompanion.insert(
|
||||
tripId: p.tripId,
|
||||
segmentId: p.segmentId,
|
||||
timestamp: p.timestamp,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
speedKmh: p.speedKmh,
|
||||
altitudeM: p.altitudeM,
|
||||
accuracyM: Value(p.accuracyM),
|
||||
bearingDeg: Value(p.bearingDeg),
|
||||
synced: Value(p.synced),
|
||||
));
|
||||
Future<int> insertPoint(domain.TrackPoint p) => into(trackPoints).insert(
|
||||
TrackPointsCompanion.insert(
|
||||
tripId: p.tripId,
|
||||
segmentId: p.segmentId,
|
||||
timestamp: p.timestamp,
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
speedKmh: p.speedKmh,
|
||||
altitudeM: p.altitudeM,
|
||||
accuracyM: Value(p.accuracyM),
|
||||
bearingDeg: Value(p.bearingDeg),
|
||||
synced: Value(p.synced),
|
||||
),
|
||||
);
|
||||
|
||||
/// Ordered by segment then id so consumers walk the ride in recording order with pause
|
||||
/// boundaries intact. **Not** ordered by timestamp: that value is GPS-derived and can
|
||||
/// jump, whereas id is monotonic in write order.
|
||||
Future<List<domain.TrackPoint>> pointsForTrip(int tripId) async {
|
||||
final rows = await (select(trackPoints)
|
||||
..where((p) => p.tripId.equals(tripId))
|
||||
..orderBy([
|
||||
(p) => OrderingTerm.asc(p.segmentId),
|
||||
(p) => OrderingTerm.asc(p.id),
|
||||
]))
|
||||
.get();
|
||||
final rows =
|
||||
await (select(trackPoints)
|
||||
..where((p) => p.tripId.equals(tripId))
|
||||
..orderBy([
|
||||
(p) => OrderingTerm.asc(p.segmentId),
|
||||
(p) => OrderingTerm.asc(p.id),
|
||||
]))
|
||||
.get();
|
||||
return rows.map(_toPoint).toList();
|
||||
}
|
||||
|
||||
Future<List<domain.TrackPoint>> pointsForSegment(int segmentId) async {
|
||||
final rows = await (select(trackPoints)
|
||||
..where((p) => p.segmentId.equals(segmentId))
|
||||
..orderBy([(p) => OrderingTerm.asc(p.id)]))
|
||||
.get();
|
||||
final rows =
|
||||
await (select(trackPoints)
|
||||
..where((p) => p.segmentId.equals(segmentId))
|
||||
..orderBy([(p) => OrderingTerm.asc(p.id)]))
|
||||
.get();
|
||||
return rows.map(_toPoint).toList();
|
||||
}
|
||||
|
||||
/// The anchor a restarted recorder needs to continue accumulating distance.
|
||||
Future<domain.TrackPoint?> lastInSegment(int segmentId) async {
|
||||
final row = await (select(trackPoints)
|
||||
..where((p) => p.segmentId.equals(segmentId))
|
||||
..orderBy([(p) => OrderingTerm.desc(p.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
final row =
|
||||
await (select(trackPoints)
|
||||
..where((p) => p.segmentId.equals(segmentId))
|
||||
..orderBy([(p) => OrderingTerm.desc(p.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toPoint(row);
|
||||
}
|
||||
|
||||
Future<int> countPointsForTrip(int tripId) async =>
|
||||
(await (select(trackPoints)..where((p) => p.tripId.equals(tripId))).get())
|
||||
.length;
|
||||
Future<int> countPointsForTrip(int tripId) async => (await (select(
|
||||
trackPoints,
|
||||
)..where((p) => p.tripId.equals(tripId))).get()).length;
|
||||
|
||||
/// Live stats for one trip.
|
||||
///
|
||||
@@ -342,14 +357,16 @@ class AppDatabase extends _$AppDatabase {
|
||||
variables: [Variable.withInt(tripId)],
|
||||
readsFrom: {trackPoints},
|
||||
);
|
||||
return q.watchSingle().map((row) => domain.RideStats(
|
||||
pointCount: row.read<int>('pointCount'),
|
||||
maxSpeedKmh: row.read<double>('maxSpeedKmh'),
|
||||
avgSpeedKmh: row.read<double>('avgSpeedKmh'),
|
||||
firstTimestamp: row.read<int>('firstTimestamp'),
|
||||
lastTimestamp: row.read<int>('lastTimestamp'),
|
||||
pendingUpload: row.read<int>('pendingUpload'),
|
||||
));
|
||||
return q.watchSingle().map(
|
||||
(row) => domain.RideStats(
|
||||
pointCount: row.read<int>('pointCount'),
|
||||
maxSpeedKmh: row.read<double>('maxSpeedKmh'),
|
||||
avgSpeedKmh: row.read<double>('avgSpeedKmh'),
|
||||
firstTimestamp: row.read<int>('firstTimestamp'),
|
||||
lastTimestamp: row.read<int>('lastTimestamp'),
|
||||
pendingUpload: row.read<int>('pendingUpload'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Upload backlog ------------------------------------------------------
|
||||
@@ -357,35 +374,38 @@ class AppDatabase extends _$AppDatabase {
|
||||
// the payload carries trip/segment identity per point rather than per batch.
|
||||
|
||||
Future<List<domain.TrackPoint>> unsyncedPoints(int limit) async {
|
||||
final rows = await (select(trackPoints)
|
||||
..where((p) => p.synced.equals(false))
|
||||
..orderBy([(p) => OrderingTerm.asc(p.id)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
final rows =
|
||||
await (select(trackPoints)
|
||||
..where((p) => p.synced.equals(false))
|
||||
..orderBy([(p) => OrderingTerm.asc(p.id)])
|
||||
..limit(limit))
|
||||
.get();
|
||||
return rows.map(_toPoint).toList();
|
||||
}
|
||||
|
||||
Future<void> markSynced(List<int> ids) async {
|
||||
if (ids.isEmpty) return;
|
||||
await (update(trackPoints)..where((p) => p.id.isIn(ids)))
|
||||
.write(const TrackPointsCompanion(synced: Value(true)));
|
||||
await (update(trackPoints)..where((p) => p.id.isIn(ids))).write(
|
||||
const TrackPointsCompanion(synced: Value(true)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> countUnsynced() async =>
|
||||
(await (select(trackPoints)..where((p) => p.synced.equals(false))).get())
|
||||
.length;
|
||||
Future<int> countUnsynced() async => (await (select(
|
||||
trackPoints,
|
||||
)..where((p) => p.synced.equals(false))).get()).length;
|
||||
|
||||
// --- Maintenance ---------------------------------------------------------
|
||||
|
||||
/// Used by merge: points carry tripId directly, so they re-parent alongside segments.
|
||||
Future<void> reparentPoints(int oldTripId, int newTripId) =>
|
||||
(update(trackPoints)..where((p) => p.tripId.equals(oldTripId)))
|
||||
.write(TrackPointsCompanion(tripId: Value(newTripId)));
|
||||
(update(trackPoints)..where((p) => p.tripId.equals(oldTripId))).write(
|
||||
TrackPointsCompanion(tripId: Value(newTripId)),
|
||||
);
|
||||
|
||||
Future<List<domain.TrackPoint>> allPoints() async {
|
||||
final rows = await (select(trackPoints)
|
||||
..orderBy([(p) => OrderingTerm.asc(p.id)]))
|
||||
.get();
|
||||
final rows = await (select(
|
||||
trackPoints,
|
||||
)..orderBy([(p) => OrderingTerm.asc(p.id)])).get();
|
||||
return rows.map(_toPoint).toList();
|
||||
}
|
||||
}
|
||||
@@ -395,35 +415,35 @@ class AppDatabase extends _$AppDatabase {
|
||||
// entirely unaware that Drift exists.
|
||||
|
||||
domain.Trip _toTrip(TripRow r) => domain.Trip(
|
||||
id: r.id,
|
||||
startedAt: r.startedAt,
|
||||
endedAt: r.endedAt,
|
||||
name: r.name,
|
||||
state: r.state,
|
||||
distanceM: r.distanceM,
|
||||
movingMillis: r.movingMillis,
|
||||
maxSpeedKmh: r.maxSpeedKmh,
|
||||
elevationGainM: r.elevationGainM,
|
||||
pointCount: r.pointCount,
|
||||
);
|
||||
id: r.id,
|
||||
startedAt: r.startedAt,
|
||||
endedAt: r.endedAt,
|
||||
name: r.name,
|
||||
state: r.state,
|
||||
distanceM: r.distanceM,
|
||||
movingMillis: r.movingMillis,
|
||||
maxSpeedKmh: r.maxSpeedKmh,
|
||||
elevationGainM: r.elevationGainM,
|
||||
pointCount: r.pointCount,
|
||||
);
|
||||
|
||||
domain.Segment _toSegment(SegmentRow r) => domain.Segment(
|
||||
id: r.id,
|
||||
tripId: r.tripId,
|
||||
startedAt: r.startedAt,
|
||||
endedAt: r.endedAt,
|
||||
);
|
||||
id: r.id,
|
||||
tripId: r.tripId,
|
||||
startedAt: r.startedAt,
|
||||
endedAt: r.endedAt,
|
||||
);
|
||||
|
||||
domain.TrackPoint _toPoint(TrackPointRow r) => domain.TrackPoint(
|
||||
id: r.id,
|
||||
tripId: r.tripId,
|
||||
segmentId: r.segmentId,
|
||||
timestamp: r.timestamp,
|
||||
latitude: r.latitude,
|
||||
longitude: r.longitude,
|
||||
speedKmh: r.speedKmh,
|
||||
altitudeM: r.altitudeM,
|
||||
accuracyM: r.accuracyM,
|
||||
bearingDeg: r.bearingDeg,
|
||||
synced: r.synced,
|
||||
);
|
||||
id: r.id,
|
||||
tripId: r.tripId,
|
||||
segmentId: r.segmentId,
|
||||
timestamp: r.timestamp,
|
||||
latitude: r.latitude,
|
||||
longitude: r.longitude,
|
||||
speedKmh: r.speedKmh,
|
||||
altitudeM: r.altitudeM,
|
||||
accuracyM: r.accuracyM,
|
||||
bearingDeg: r.bearingDeg,
|
||||
synced: r.synced,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user