fix: trip builder handles NULL-GPS speed-only rows (BUG-16 follow-up)

Trips starting/ending on speed-only rows violated the trips.*GPSX NOT NULL
constraint. Use the first/last GPS-bearing row for start/end coordinates, and
skip a trip that never got any GPS fix (no location to record).
This commit is contained in:
Mathieu Moran
2026-08-13 15:52:07 -04:00
parent e5ac14e38b
commit f5f20c315b
2 changed files with 36 additions and 9 deletions
+16 -4
View File
@@ -198,16 +198,28 @@ def build_trip_summaries(raw_rows):
if not any(event["obd"] for event in events) and duration_seconds < 10: if not any(event["obd"] for event in events) and duration_seconds < 10:
continue continue
# Speed-only rows (logged before the first GPS fix) have NULL coords.
# Use the first/last row that actually has a position for start/end GPS,
# and skip a trip that never got any fix at all (no location to record).
gps_rows = [
r for r in trip_rows
if r["latitude"] is not None and r["longitude"] is not None
]
if not gps_rows:
continue
start_gps = gps_rows[0]
end_gps = gps_rows[-1]
trips.append({ trips.append({
"id": start["id"], "id": start["id"],
"start_tracking_id": start["id"], "start_tracking_id": start["id"],
"end_tracking_id": end["id"], "end_tracking_id": end["id"],
"start_datetime": start["datetime"].isoformat(sep=" "), "start_datetime": start["datetime"].isoformat(sep=" "),
"end_datetime": end["datetime"].isoformat(sep=" "), "end_datetime": end["datetime"].isoformat(sep=" "),
"start_gpsx": start["latitude"], "start_gpsx": start_gps["latitude"],
"start_gpsy": start["longitude"], "start_gpsy": start_gps["longitude"],
"end_gpsx": end["latitude"], "end_gpsx": end_gps["latitude"],
"end_gpsy": end["longitude"], "end_gpsy": end_gps["longitude"],
"distance_km": round(distance_km, 3), "distance_km": round(distance_km, 3),
"duration_seconds": duration_seconds, "duration_seconds": duration_seconds,
"point_count": len(trip_rows), "point_count": len(trip_rows),
+20 -5
View File
@@ -115,7 +115,7 @@ class TripBuilderTest(unittest.TestCase):
self.assertEqual(trips[0]["start_tracking_id"], 1) self.assertEqual(trips[0]["start_tracking_id"], 1)
self.assertEqual(trips[0]["end_tracking_id"], 5) self.assertEqual(trips[0]["end_tracking_id"], 5)
def test_speed_only_rows_without_gps_build_trip(self): def test_speed_only_rows_without_any_gps_are_skipped(self):
started = datetime(2026, 1, 1, 8) started = datetime(2026, 1, 1, 8)
rows = [ rows = [
(index + 1, started + timedelta(seconds=index * 5), None, None, 30) (index + 1, started + timedelta(seconds=index * 5), None, None, 30)
@@ -124,12 +124,27 @@ class TripBuilderTest(unittest.TestCase):
trips = app_trip.build_trip_summaries(rows) trips = app_trip.build_trip_summaries(rows)
# No GPS fix at all -> no location to record -> no trip (the speed-only
# rows remain in the DB but can't form a geolocated trip).
self.assertEqual(len(trips), 0)
def test_speed_only_start_uses_first_gps_fix_for_start_coords(self):
started = datetime(2026, 1, 1, 8)
rows = [
(1, started, None, None, 30),
(2, started + timedelta(seconds=5), None, None, 30),
(3, started + timedelta(seconds=10), 45.5000, -73.5000, 30),
(4, started + timedelta(seconds=15), 45.5001, -73.5000, 30),
(5, started + timedelta(seconds=20), 45.5002, -73.5000, 30),
]
trips = app_trip.build_trip_summaries(rows)
self.assertEqual(len(trips), 1) self.assertEqual(len(trips), 1)
self.assertEqual(trips[0]["start_tracking_id"], 1) self.assertEqual(trips[0]["start_tracking_id"], 1)
self.assertEqual(trips[0]["end_tracking_id"], 4) self.assertEqual(trips[0]["end_tracking_id"], 5)
self.assertEqual(trips[0]["distance_km"], 0) self.assertEqual(trips[0]["start_gpsx"], 45.5)
self.assertEqual(trips[0]["max_speed_kmh"], 30) self.assertEqual(trips[0]["start_gpsy"], -73.5)
self.assertEqual(trips[0]["point_count"], 4)
def test_clock_rollback_does_not_merge_motion_episodes(self): def test_clock_rollback_does_not_merge_motion_episodes(self):
first = self.rows_for_route( first = self.rows_for_route(