From 0d5aea594732eb7e96cfc87785814173ef1476a6 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Wed, 12 Aug 2026 16:59:00 -0400 Subject: [PATCH] feat: simulated odometer in trip replay + trip list (continuous across trips) - Pi route_points: emit per-point odometer (raw OBD value or baseline+prior+within-trip distance) - Pi get_trip_route: SELECT ODOMETER, compute prior-trips distance + baseline - Pi list_trips: fallback now starts from baseline (was 0) - Server get_trip_route: per-point odometer (raw or simulated) with haversine accumulation - Server list_trips: odo_start/end from baseline + cumulative (was hardcoded None) - history.js: Odometer line in replay scrubber below speed - Both history templates: trip-replay-odometer element --- modules/app_sql.py | 24 +++++++-- modules/app_trip.py | 15 +++++- server/modules/db.py | 96 ++++++++++++++++++++++++++--------- server/templates/history.html | 1 + static/js/history.js | 6 +++ templates/history.html | 1 + 6 files changed, 114 insertions(+), 29 deletions(-) diff --git a/modules/app_sql.py b/modules/app_sql.py index 244c633..6a1d678 100644 --- a/modules/app_sql.py +++ b/modules/app_sql.py @@ -487,9 +487,12 @@ def list_trips(): START_ODOMETER, END_ODOMETER FROM trips ORDER BY START_DATETIME ASC""" ).fetchall() - # If real OBD odometer is available use it; otherwise fall back to cumulative distance + # If real OBD odometer is available use it; otherwise fall back to + # baseline + cumulative distance (continuous across trips). + from modules import app_settings + baseline = app_settings._number_value("odometer_baseline", float, 0) result = [] - cumulative_km = 0.0 + cumulative_km = baseline for row in rows: dist = row[7] or 0 start_odo = row[12] if row[12] is not None else round(cumulative_km, 1) @@ -530,11 +533,19 @@ def get_trip_route(trip_id, max_points=2000): if trip is None: return None rows = conn.execute( - """SELECT id, DATETIME, GPSX, GPSY, SPEED + """SELECT id, DATETIME, GPSX, GPSY, SPEED, ODOMETER FROM tracking WHERE id BETWEEN ? AND ? ORDER BY DATETIME ASC, id ASC""", (trip[8], trip[9]), ).fetchall() + prior = conn.execute( + """SELECT COALESCE(SUM(DISTANCE_KM), 0) + FROM trips WHERE START_DATETIME < ?""", + (trip[1],), + ).fetchone() + from modules import app_settings + baseline = app_settings._number_value("odometer_baseline", float, 0) + prior_km = prior[0] if prior and prior[0] is not None else 0.0 return { "trip": { "id": trip[0], @@ -546,7 +557,12 @@ def get_trip_route(trip_id, max_points=2000): "average_speed_kmh": trip[6], "max_speed_kmh": trip[7], }, - "points": app_trip.route_points(rows, max_points=max_points), + "points": app_trip.route_points( + rows, + max_points=max_points, + odometer_baseline=baseline, + prior_km=prior_km, + ), } diff --git a/modules/app_trip.py b/modules/app_trip.py index ae1729f..3c70960 100644 --- a/modules/app_trip.py +++ b/modules/app_trip.py @@ -216,7 +216,7 @@ def build_trip_summaries(raw_rows): return trips -def route_points(raw_rows, max_points=2000): +def route_points(raw_rows, max_points=2000, odometer_baseline=0.0, prior_km=0.0): rows = normalize_tracking_rows(raw_rows) filtered_rows = [] for row in rows: @@ -226,6 +226,7 @@ def route_points(raw_rows, max_points=2000): continue filtered_rows.append(row) rows = filtered_rows + cumulative_km = 0.0 for index, row in enumerate(rows): row["speed_source"] = "obd" if row["speed"] > 0 else "gps" if row["speed"] <= 0 and index: @@ -235,6 +236,17 @@ def route_points(raw_rows, max_points=2000): if distance_km * 1000 >= GPS_MOTION_METERS else 0 ) + if index: + segment_km, _ = _segment(row, rows[index - 1]) + cumulative_km += segment_km + # Odometer per point: use the raw OBD value when the vehicle reports + # one; otherwise simulate it as baseline + prior trips + within-trip + # distance (continuous across trips, matching get_overview_stats). + raw_odometer = row.get("odometer") + if raw_odometer is not None and raw_odometer > 0: + row["odometer"] = raw_odometer + else: + row["odometer"] = odometer_baseline + prior_km + cumulative_km if len(rows) <= max_points: selected = rows else: @@ -250,4 +262,5 @@ def route_points(raw_rows, max_points=2000): "longitude": row["longitude"], "speed": round(row["speed"], 1), "speed_source": row["speed_source"], + "odometer": round(row["odometer"], 1) if row["odometer"] is not None else None, } for row in selected] diff --git a/server/modules/db.py b/server/modules/db.py index 8bc10dc..30576f7 100644 --- a/server/modules/db.py +++ b/server/modules/db.py @@ -514,6 +514,11 @@ def compute_trips_for_device(device_id): def list_trips(device_id, limit=200): with get_cursor() as cur: + cur.execute( + "SELECT odometer_baseline FROM devices WHERE id = %s", + (device_id,), + ) + base_row = cur.fetchone() cur.execute( """ SELECT id, start_time, end_time, distance_km, duration_s, @@ -521,18 +526,22 @@ def list_trips(device_id, limit=200): end_lat, end_lon, point_count FROM trips WHERE device_id = %s - ORDER BY start_time DESC - LIMIT %s + ORDER BY start_time ASC """, - (device_id, limit), + (device_id,), ) rows = cur.fetchall() - return [ - { + baseline = float(base_row[0]) if base_row and base_row[0] is not None else 0.0 + # Simulate a continuous odometer: baseline + cumulative trip distance. + cumulative = baseline + trips = [] + for r in rows: + dist = float(r[3]) if r[3] is not None else 0.0 + trips.append({ "id": r[0], "start_datetime": r[1].isoformat(), "end_datetime": r[2].isoformat(), - "distance_km": r[3], + "distance_km": dist, "duration_seconds": r[4], "average_speed_kmh": r[5], "max_speed_kmh": r[6], @@ -541,11 +550,12 @@ def list_trips(device_id, limit=200): "end_lat": r[9], "end_lon": r[10], "point_count": r[11], - "odo_start_km": None, - "odo_end_km": None, - } - for r in rows - ] + "odo_start_km": round(cumulative, 1), + "odo_end_km": round(cumulative + dist, 1), + }) + cumulative += dist + trips.reverse() # newest first for display + return trips[:limit] def get_trip_route(device_id, trip_id): @@ -559,7 +569,7 @@ def get_trip_route(device_id, trip_id): return None cur.execute( """ - SELECT recorded_at, gps_lat, gps_lon, speed + SELECT recorded_at, gps_lat, gps_lon, speed, odometer FROM tracking_points WHERE device_id = %s AND recorded_at BETWEEN %s AND %s AND gps_lat IS NOT NULL AND gps_lon IS NOT NULL @@ -568,6 +578,55 @@ def get_trip_route(device_id, trip_id): (device_id, trip_row[0], trip_row[1]), ) points = cur.fetchall() + cur.execute( + "SELECT COALESCE(SUM(distance_km), 0) FROM trips WHERE device_id = %s AND start_time < %s", + (device_id, trip_row[0]), + ) + prior = cur.fetchone() + cur.execute( + "SELECT odometer_baseline FROM devices WHERE id = %s", + (device_id,), + ) + base_row = cur.fetchone() + baseline = float(base_row[0]) if base_row and base_row[0] is not None else 0.0 + prior_km = float(prior[0]) if prior and prior[0] is not None else 0.0 + + import math + + def hav(lat1, lon1, lat2, lon2): + r = 6371.0088 + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + a = (math.sin(dlat / 2) ** 2 + + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) + * math.sin(dlon / 2) ** 2) + return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + result_points = [] + cumulative = 0.0 + prev = None + for p in points: + lat = float(p[1]) + lon = float(p[2]) + speed = p[3] or 0 + raw_odo = p[4] + if prev is not None: + cumulative += hav(prev[0], prev[1], lat, lon) + prev = (lat, lon) + if raw_odo is not None and float(raw_odo) > 0: + odometer = float(raw_odo) + else: + odometer = baseline + prior_km + cumulative + result_points.append({ + "datetime": p[0].isoformat(), + "latitude": lat, + "longitude": lon, + "speed": speed, + # Mirror the Pi's route_points: OBD speed when we have it, + # otherwise the point is sourced from GPS. + "speed_source": "obd" if speed > 0 else "gps", + "odometer": round(odometer, 1), + }) return { "trip": { "distance_km": trip_row[2], @@ -575,18 +634,7 @@ def get_trip_route(device_id, trip_id): "average_speed_kmh": trip_row[4], "max_speed_kmh": trip_row[5], }, - "points": [ - { - "datetime": p[0].isoformat(), - "latitude": p[1], - "longitude": p[2], - "speed": p[3] or 0, - # Mirror the Pi's route_points: OBD speed when we have it, - # otherwise the point is sourced from GPS. - "speed_source": "obd" if (p[3] or 0) > 0 else "gps", - } - for p in points - ], + "points": result_points, } diff --git a/server/templates/history.html b/server/templates/history.html index 0bd5c10..9e1cd20 100644 --- a/server/templates/history.html +++ b/server/templates/history.html @@ -78,6 +78,7 @@ +
diff --git a/static/js/history.js b/static/js/history.js index 029c083..a3b27dd 100644 --- a/static/js/history.js +++ b/static/js/history.js @@ -128,6 +128,12 @@ st.marker.setLatLng([p.latitude, p.longitude]); scrubber.value = i; replayTime.textContent = fT(p.datetime); replaySpeed.textContent = (p.speed_source === "obd" ? "OBD" : "GPS") + ": " + fS(p.speed); + var odoEl = document.getElementById("trip-replay-odometer"); + if (odoEl) { + odoEl.textContent = (p.odometer != null) + ? "Odometer: " + p.odometer.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " km" + : ""; + } schedLoc(p.latitude, p.longitude); } function initTL(pts) { diff --git a/templates/history.html b/templates/history.html index 0567957..642b794 100644 --- a/templates/history.html +++ b/templates/history.html @@ -70,6 +70,7 @@ +