/// Pure conversion and serialization logic, free of Flutter and platform types. /// /// Ported from `com.rippr.Telemetry`. The Kotlin original also held process-wide /// recording state; that was already deleted in v2 (state lives in the database, because /// it must survive process death) and is not resurrected here. library; import 'dart:convert'; import '../domain/models.dart'; const double msToKmhFactor = 3.6; double msToKmh(double metersPerSecond) => metersPerSecond * msToKmhFactor; /// A parked bike still emits jittering fixes. Anything under this is reported as zero so /// "max speed" is not set by GPS noise while the phone sits in a pocket. const double speedNoiseFloorKmh = 1.5; double sanitizeSpeedKmh(double raw) { if (!raw.isFinite || raw < speedNoiseFloorKmh) return 0.0; return raw; } /// Drop fixes too imprecise to be worth storing. 0 means "accuracy unknown". bool isUsableFix(double accuracyMeters, {double maxAccuracyMeters = 50.0}) => accuracyMeters <= 0.0 || accuracyMeters <= maxAccuracyMeters; String formatDuration(int millis) { if (millis <= 0) return '00:00:00'; final totalSeconds = millis ~/ 1000; final h = (totalSeconds ~/ 3600).toString().padLeft(2, '0'); final m = ((totalSeconds % 3600) ~/ 60).toString().padLeft(2, '0'); final s = (totalSeconds % 60).toString().padLeft(2, '0'); return '$h:$m:$s'; } /// Encode a batch of points for the upload endpoint. /// /// The trip and segment ids are written **per point, not per batch**: the unsynced-point /// query draws by id and can straddle a segment or, after a discard-and-restart, a trip /// boundary. Hoisting them to batch level would silently mislabel points. String encodeBatch(String deviceId, List points) { final array = points .map((p) => { 'id': p.id, 'trip_id': p.tripId, 'segment_id': p.segmentId, 'ts': p.timestamp, 'lat': p.latitude, 'lon': p.longitude, 'speed_kmh': p.speedKmh, 'alt_m': p.altitudeM, 'acc_m': p.accuracyM, 'bearing': p.bearingDeg, }) .toList(growable: false); return jsonEncode({ 'device_id': deviceId, 'points': array, }); }