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
This commit is contained in:
+20
-4
@@ -487,9 +487,12 @@ def list_trips():
|
|||||||
START_ODOMETER, END_ODOMETER
|
START_ODOMETER, END_ODOMETER
|
||||||
FROM trips ORDER BY START_DATETIME ASC"""
|
FROM trips ORDER BY START_DATETIME ASC"""
|
||||||
).fetchall()
|
).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 = []
|
result = []
|
||||||
cumulative_km = 0.0
|
cumulative_km = baseline
|
||||||
for row in rows:
|
for row in rows:
|
||||||
dist = row[7] or 0
|
dist = row[7] or 0
|
||||||
start_odo = row[12] if row[12] is not None else round(cumulative_km, 1)
|
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:
|
if trip is None:
|
||||||
return None
|
return None
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""SELECT id, DATETIME, GPSX, GPSY, SPEED
|
"""SELECT id, DATETIME, GPSX, GPSY, SPEED, ODOMETER
|
||||||
FROM tracking WHERE id BETWEEN ? AND ?
|
FROM tracking WHERE id BETWEEN ? AND ?
|
||||||
ORDER BY DATETIME ASC, id ASC""",
|
ORDER BY DATETIME ASC, id ASC""",
|
||||||
(trip[8], trip[9]),
|
(trip[8], trip[9]),
|
||||||
).fetchall()
|
).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 {
|
return {
|
||||||
"trip": {
|
"trip": {
|
||||||
"id": trip[0],
|
"id": trip[0],
|
||||||
@@ -546,7 +557,12 @@ def get_trip_route(trip_id, max_points=2000):
|
|||||||
"average_speed_kmh": trip[6],
|
"average_speed_kmh": trip[6],
|
||||||
"max_speed_kmh": trip[7],
|
"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,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -216,7 +216,7 @@ def build_trip_summaries(raw_rows):
|
|||||||
return trips
|
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)
|
rows = normalize_tracking_rows(raw_rows)
|
||||||
filtered_rows = []
|
filtered_rows = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -226,6 +226,7 @@ def route_points(raw_rows, max_points=2000):
|
|||||||
continue
|
continue
|
||||||
filtered_rows.append(row)
|
filtered_rows.append(row)
|
||||||
rows = filtered_rows
|
rows = filtered_rows
|
||||||
|
cumulative_km = 0.0
|
||||||
for index, row in enumerate(rows):
|
for index, row in enumerate(rows):
|
||||||
row["speed_source"] = "obd" if row["speed"] > 0 else "gps"
|
row["speed_source"] = "obd" if row["speed"] > 0 else "gps"
|
||||||
if row["speed"] <= 0 and index:
|
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
|
if distance_km * 1000 >= GPS_MOTION_METERS
|
||||||
else 0
|
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:
|
if len(rows) <= max_points:
|
||||||
selected = rows
|
selected = rows
|
||||||
else:
|
else:
|
||||||
@@ -250,4 +262,5 @@ def route_points(raw_rows, max_points=2000):
|
|||||||
"longitude": row["longitude"],
|
"longitude": row["longitude"],
|
||||||
"speed": round(row["speed"], 1),
|
"speed": round(row["speed"], 1),
|
||||||
"speed_source": row["speed_source"],
|
"speed_source": row["speed_source"],
|
||||||
|
"odometer": round(row["odometer"], 1) if row["odometer"] is not None else None,
|
||||||
} for row in selected]
|
} for row in selected]
|
||||||
|
|||||||
+72
-24
@@ -514,6 +514,11 @@ def compute_trips_for_device(device_id):
|
|||||||
|
|
||||||
def list_trips(device_id, limit=200):
|
def list_trips(device_id, limit=200):
|
||||||
with get_cursor() as cur:
|
with get_cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT odometer_baseline FROM devices WHERE id = %s",
|
||||||
|
(device_id,),
|
||||||
|
)
|
||||||
|
base_row = cur.fetchone()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, start_time, end_time, distance_km, duration_s,
|
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
|
end_lat, end_lon, point_count
|
||||||
FROM trips
|
FROM trips
|
||||||
WHERE device_id = %s
|
WHERE device_id = %s
|
||||||
ORDER BY start_time DESC
|
ORDER BY start_time ASC
|
||||||
LIMIT %s
|
|
||||||
""",
|
""",
|
||||||
(device_id, limit),
|
(device_id,),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
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],
|
"id": r[0],
|
||||||
"start_datetime": r[1].isoformat(),
|
"start_datetime": r[1].isoformat(),
|
||||||
"end_datetime": r[2].isoformat(),
|
"end_datetime": r[2].isoformat(),
|
||||||
"distance_km": r[3],
|
"distance_km": dist,
|
||||||
"duration_seconds": r[4],
|
"duration_seconds": r[4],
|
||||||
"average_speed_kmh": r[5],
|
"average_speed_kmh": r[5],
|
||||||
"max_speed_kmh": r[6],
|
"max_speed_kmh": r[6],
|
||||||
@@ -541,11 +550,12 @@ def list_trips(device_id, limit=200):
|
|||||||
"end_lat": r[9],
|
"end_lat": r[9],
|
||||||
"end_lon": r[10],
|
"end_lon": r[10],
|
||||||
"point_count": r[11],
|
"point_count": r[11],
|
||||||
"odo_start_km": None,
|
"odo_start_km": round(cumulative, 1),
|
||||||
"odo_end_km": None,
|
"odo_end_km": round(cumulative + dist, 1),
|
||||||
}
|
})
|
||||||
for r in rows
|
cumulative += dist
|
||||||
]
|
trips.reverse() # newest first for display
|
||||||
|
return trips[:limit]
|
||||||
|
|
||||||
|
|
||||||
def get_trip_route(device_id, trip_id):
|
def get_trip_route(device_id, trip_id):
|
||||||
@@ -559,7 +569,7 @@ def get_trip_route(device_id, trip_id):
|
|||||||
return None
|
return None
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT recorded_at, gps_lat, gps_lon, speed
|
SELECT recorded_at, gps_lat, gps_lon, speed, odometer
|
||||||
FROM tracking_points
|
FROM tracking_points
|
||||||
WHERE device_id = %s AND recorded_at BETWEEN %s AND %s
|
WHERE device_id = %s AND recorded_at BETWEEN %s AND %s
|
||||||
AND gps_lat IS NOT NULL AND gps_lon IS NOT NULL
|
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]),
|
(device_id, trip_row[0], trip_row[1]),
|
||||||
)
|
)
|
||||||
points = cur.fetchall()
|
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 {
|
return {
|
||||||
"trip": {
|
"trip": {
|
||||||
"distance_km": trip_row[2],
|
"distance_km": trip_row[2],
|
||||||
@@ -575,18 +634,7 @@ def get_trip_route(device_id, trip_id):
|
|||||||
"average_speed_kmh": trip_row[4],
|
"average_speed_kmh": trip_row[4],
|
||||||
"max_speed_kmh": trip_row[5],
|
"max_speed_kmh": trip_row[5],
|
||||||
},
|
},
|
||||||
"points": [
|
"points": result_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
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
<strong id="trip-replay-time"></strong>
|
<strong id="trip-replay-time"></strong>
|
||||||
<span id="trip-replay-speed" class="text-muted"></span>
|
<span id="trip-replay-speed" class="text-muted"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="trip-replay-odometer" class="text-muted mt-1" style="font-size:0.75rem;min-height:1rem"></div>
|
||||||
<div id="trip-replay-location" class="text-muted mt-1" style="font-size:0.75rem;min-height:1rem"></div>
|
<div id="trip-replay-location" class="text-muted mt-1" style="font-size:0.75rem;min-height:1rem"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -128,6 +128,12 @@
|
|||||||
st.marker.setLatLng([p.latitude, p.longitude]);
|
st.marker.setLatLng([p.latitude, p.longitude]);
|
||||||
scrubber.value = i; replayTime.textContent = fT(p.datetime);
|
scrubber.value = i; replayTime.textContent = fT(p.datetime);
|
||||||
replaySpeed.textContent = (p.speed_source === "obd" ? "OBD" : "GPS") + ": " + fS(p.speed);
|
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);
|
schedLoc(p.latitude, p.longitude);
|
||||||
}
|
}
|
||||||
function initTL(pts) {
|
function initTL(pts) {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
<strong id="trip-replay-time"></strong>
|
<strong id="trip-replay-time"></strong>
|
||||||
<span id="trip-replay-speed" class="text-muted"></span>
|
<span id="trip-replay-speed" class="text-muted"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="trip-replay-odometer" class="text-muted mt-1" style="font-size:0.75rem;min-height:1rem"></div>
|
||||||
<div id="trip-replay-location" class="text-muted mt-1" style="font-size:0.75rem;min-height:1rem"></div>
|
<div id="trip-replay-location" class="text-muted mt-1" style="font-size:0.75rem;min-height:1rem"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user