Add trip history and route replay
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+9
-1
@@ -14,6 +14,7 @@ SENSOR_POLL_INTERVAL = 5
|
||||
|
||||
status = None
|
||||
speed = 0
|
||||
speed_updated_at = None
|
||||
rpm = None
|
||||
coolant_temp = None
|
||||
fuel_level = None
|
||||
@@ -233,6 +234,12 @@ def get_speedtime():
|
||||
return [time_now, speed]
|
||||
|
||||
|
||||
def get_fresh_speed(max_age=5):
|
||||
if speed_updated_at is None or time.monotonic() - speed_updated_at > max_age:
|
||||
return None
|
||||
return speed
|
||||
|
||||
|
||||
def obd_connection():
|
||||
"""Emit current state for a newly connected browser and ensure OBD is running."""
|
||||
start_obd()
|
||||
@@ -245,7 +252,7 @@ def obd_connection():
|
||||
|
||||
|
||||
def obd_speed():
|
||||
global speed
|
||||
global speed, speed_updated_at
|
||||
with obd_speed_lock:
|
||||
while not stop_speed.is_set() and _is_car_connected():
|
||||
response = _query(obd.commands.SPEED)
|
||||
@@ -256,6 +263,7 @@ def obd_speed():
|
||||
print(app_gps.get_gps())
|
||||
else:
|
||||
speed = round(response.value.to("kph").magnitude, 1)
|
||||
speed_updated_at = time.monotonic()
|
||||
print("Speed:", speed, "kph")
|
||||
|
||||
app_events.socketio.emit("obd_speed", get_speedtime())
|
||||
|
||||
+251
-5
@@ -6,6 +6,7 @@ from sqlite3 import Error
|
||||
from threading import Thread, Lock
|
||||
|
||||
from modules import app_car, app_account, app_gps
|
||||
from modules import app_trip
|
||||
|
||||
# Resolve db/sql/ relative to the repo root regardless of the process's cwd
|
||||
# (previously a relative "../db/sql/" path, which broke depending on how/from
|
||||
@@ -20,10 +21,16 @@ sql_file = account.name + "_" + sCarModel + ".db"
|
||||
sql_path = os.path.join(_SQL_DIR, sql_file)
|
||||
|
||||
LOG_INTERVAL = 5 # seconds between trip-tracking snapshots
|
||||
IDLE_LOG_INTERVAL = 60
|
||||
GPS_LOG_DISTANCE_METERS = 8
|
||||
TRIP_REFRESH_INTERVAL = 30
|
||||
TRIP_REBUILD_OVERLAP_ROWS = 120
|
||||
TRIP_BUILD_VERSION = 2
|
||||
|
||||
_conn = None
|
||||
_conn_lock = Lock()
|
||||
_log_thread = None
|
||||
_last_trip_refresh = 0
|
||||
|
||||
SQL_CREATE_TRACKING_TABLE = """CREATE TABLE IF NOT EXISTS tracking (
|
||||
id integer PRIMARY KEY,
|
||||
@@ -69,6 +76,29 @@ SQL_CREATE_DASHCAM_HASH_CACHE_TABLE = """CREATE TABLE IF NOT EXISTS dashcam_hash
|
||||
SHA256 text NOT NULL
|
||||
);"""
|
||||
|
||||
SQL_CREATE_TRIPS_TABLE = """CREATE TABLE IF NOT EXISTS trips (
|
||||
id integer PRIMARY KEY,
|
||||
START_TRACKING_ID integer NOT NULL,
|
||||
END_TRACKING_ID integer NOT NULL,
|
||||
START_DATETIME datetime NOT NULL,
|
||||
END_DATETIME datetime NOT NULL,
|
||||
START_GPSX float NOT NULL,
|
||||
START_GPSY float NOT NULL,
|
||||
END_GPSX float NOT NULL,
|
||||
END_GPSY float NOT NULL,
|
||||
DISTANCE_KM float NOT NULL,
|
||||
DURATION_SECONDS integer NOT NULL,
|
||||
POINT_COUNT integer NOT NULL,
|
||||
AVERAGE_SPEED_KMH float NOT NULL DEFAULT 0,
|
||||
MAX_SPEED_KMH float NOT NULL DEFAULT 0
|
||||
);"""
|
||||
|
||||
SQL_CREATE_TRIP_BUILD_STATE_TABLE = """CREATE TABLE IF NOT EXISTS trip_build_state (
|
||||
id integer PRIMARY KEY CHECK (id = 1),
|
||||
LAST_TRACKING_ID integer NOT NULL,
|
||||
BUILD_VERSION integer NOT NULL DEFAULT 1
|
||||
);"""
|
||||
|
||||
|
||||
def create_conn(db_file):
|
||||
"""Open a SQLite connection to db_file, creating its parent folder if needed.
|
||||
@@ -111,7 +141,11 @@ def get_conn():
|
||||
create_table(_conn, SQL_CREATE_SETTINGS_TABLE)
|
||||
create_table(_conn, SQL_CREATE_DASHCAM_UPLOADS_TABLE)
|
||||
create_table(_conn, SQL_CREATE_DASHCAM_HASH_CACHE_TABLE)
|
||||
create_table(_conn, SQL_CREATE_TRIPS_TABLE)
|
||||
create_table(_conn, SQL_CREATE_TRIP_BUILD_STATE_TABLE)
|
||||
_migrate_tracking_table(_conn)
|
||||
_migrate_trips_table(_conn)
|
||||
_migrate_trip_build_state_table(_conn)
|
||||
else:
|
||||
print("Error! cannot create the database connection.")
|
||||
return _conn
|
||||
@@ -126,20 +160,212 @@ def _migrate_tracking_table(conn):
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _migrate_trips_table(conn):
|
||||
existing_columns = {
|
||||
row[1] for row in conn.execute("PRAGMA table_info(trips)").fetchall()
|
||||
}
|
||||
additions = {
|
||||
"AVERAGE_SPEED_KMH": "float NOT NULL DEFAULT 0",
|
||||
"MAX_SPEED_KMH": "float NOT NULL DEFAULT 0",
|
||||
}
|
||||
for name, definition in additions.items():
|
||||
if name not in existing_columns:
|
||||
conn.execute(f"ALTER TABLE trips ADD COLUMN {name} {definition}")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _migrate_trip_build_state_table(conn):
|
||||
existing_columns = {
|
||||
row[1] for row in conn.execute("PRAGMA table_info(trip_build_state)").fetchall()
|
||||
}
|
||||
if "BUILD_VERSION" not in existing_columns:
|
||||
conn.execute(
|
||||
"ALTER TABLE trip_build_state "
|
||||
"ADD COLUMN BUILD_VERSION integer NOT NULL DEFAULT 1"
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def insert_tracking(gps_x, gps_y, speed, odometer=None):
|
||||
"""Insert a single trip-tracking snapshot row (GPS position + speed)."""
|
||||
conn = get_conn()
|
||||
if conn is None:
|
||||
return
|
||||
return False
|
||||
|
||||
sql = """INSERT INTO tracking (VN, NAME, DATETIME, GPSX, GPSY, SPEED, ODOMETER)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"""
|
||||
with _conn_lock:
|
||||
try:
|
||||
conn.execute(sql, (car.model, account.name, datetime.now(), gps_x, gps_y, speed, odometer))
|
||||
latitude = float(str(gps_x).replace("\x00", "").strip())
|
||||
longitude = float(str(gps_y).replace("\x00", "").strip())
|
||||
conn.execute(
|
||||
sql,
|
||||
(car.model, account.name, datetime.now(), latitude, longitude, speed, odometer),
|
||||
)
|
||||
conn.commit()
|
||||
except Error as e:
|
||||
return True
|
||||
except (Error, TypeError, ValueError) as e:
|
||||
print("Failed to insert tracking row:", e)
|
||||
return False
|
||||
|
||||
|
||||
def should_log_tracking(position, speed, last_position, seconds_since_log):
|
||||
if last_position is None:
|
||||
return True
|
||||
if speed is not None and speed > 1:
|
||||
return True
|
||||
distance_meters = app_trip.haversine_km(last_position, position) * 1000
|
||||
return (
|
||||
distance_meters >= GPS_LOG_DISTANCE_METERS
|
||||
or seconds_since_log >= IDLE_LOG_INTERVAL
|
||||
)
|
||||
|
||||
|
||||
def refresh_trips(force=False):
|
||||
"""Rebuild the derived trip index when new raw tracking rows are available."""
|
||||
global _last_trip_refresh
|
||||
conn = get_conn()
|
||||
if conn is None:
|
||||
return 0
|
||||
with _conn_lock:
|
||||
last_tracking_id = conn.execute(
|
||||
"SELECT COALESCE(MAX(id), 0) FROM tracking"
|
||||
).fetchone()[0]
|
||||
state = conn.execute(
|
||||
"SELECT LAST_TRACKING_ID, BUILD_VERSION "
|
||||
"FROM trip_build_state WHERE id = 1"
|
||||
).fetchone()
|
||||
recently_refreshed = (
|
||||
_last_trip_refresh
|
||||
and time.monotonic() - _last_trip_refresh < TRIP_REFRESH_INTERVAL
|
||||
)
|
||||
if (
|
||||
not force
|
||||
and state is not None
|
||||
and state[1] == TRIP_BUILD_VERSION
|
||||
and (state[0] == last_tracking_id or recently_refreshed)
|
||||
):
|
||||
return conn.execute("SELECT COUNT(*) FROM trips").fetchone()[0]
|
||||
|
||||
rebuild_start_id = 1
|
||||
if not force and state is not None and state[1] == TRIP_BUILD_VERSION:
|
||||
rebuild_start_id = max(1, state[0] - TRIP_REBUILD_OVERLAP_ROWS)
|
||||
intersecting_trip = conn.execute(
|
||||
"""SELECT MIN(START_TRACKING_ID) FROM trips
|
||||
WHERE END_TRACKING_ID >= ?""",
|
||||
(rebuild_start_id,),
|
||||
).fetchone()
|
||||
if intersecting_trip[0] is not None:
|
||||
rebuild_start_id = min(rebuild_start_id, intersecting_trip[0])
|
||||
rows = conn.execute(
|
||||
"""SELECT id, DATETIME, GPSX, GPSY, SPEED
|
||||
FROM tracking WHERE id >= ? ORDER BY id ASC""",
|
||||
(rebuild_start_id,),
|
||||
).fetchall()
|
||||
|
||||
trips = app_trip.build_trip_summaries(rows)
|
||||
with _conn_lock:
|
||||
try:
|
||||
conn.execute(
|
||||
"DELETE FROM trips WHERE END_TRACKING_ID >= ?",
|
||||
(rebuild_start_id,),
|
||||
)
|
||||
conn.executemany(
|
||||
"""INSERT INTO trips (
|
||||
id, START_TRACKING_ID, END_TRACKING_ID,
|
||||
START_DATETIME, END_DATETIME,
|
||||
START_GPSX, START_GPSY, END_GPSX, END_GPSY,
|
||||
DISTANCE_KM, DURATION_SECONDS, POINT_COUNT,
|
||||
AVERAGE_SPEED_KMH, MAX_SPEED_KMH
|
||||
) VALUES (
|
||||
:id, :start_tracking_id, :end_tracking_id,
|
||||
:start_datetime, :end_datetime,
|
||||
:start_gpsx, :start_gpsy, :end_gpsx, :end_gpsy,
|
||||
:distance_km, :duration_seconds, :point_count,
|
||||
:average_speed_kmh, :max_speed_kmh
|
||||
)""",
|
||||
trips,
|
||||
)
|
||||
conn.execute(
|
||||
"""INSERT INTO trip_build_state (
|
||||
id, LAST_TRACKING_ID, BUILD_VERSION
|
||||
) VALUES (1, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE
|
||||
SET LAST_TRACKING_ID = excluded.LAST_TRACKING_ID,
|
||||
BUILD_VERSION = excluded.BUILD_VERSION""",
|
||||
(last_tracking_id, TRIP_BUILD_VERSION),
|
||||
)
|
||||
conn.commit()
|
||||
_last_trip_refresh = time.monotonic()
|
||||
except Error:
|
||||
conn.rollback()
|
||||
raise
|
||||
with _conn_lock:
|
||||
return conn.execute("SELECT COUNT(*) FROM trips").fetchone()[0]
|
||||
|
||||
|
||||
def list_trips():
|
||||
refresh_trips()
|
||||
conn = get_conn()
|
||||
if conn is None:
|
||||
return []
|
||||
with _conn_lock:
|
||||
rows = conn.execute(
|
||||
"""SELECT id, START_DATETIME, END_DATETIME,
|
||||
START_GPSX, START_GPSY, END_GPSX, END_GPSY,
|
||||
DISTANCE_KM, DURATION_SECONDS, POINT_COUNT,
|
||||
AVERAGE_SPEED_KMH, MAX_SPEED_KMH
|
||||
FROM trips ORDER BY START_DATETIME DESC"""
|
||||
).fetchall()
|
||||
return [{
|
||||
"id": row[0],
|
||||
"start_datetime": row[1],
|
||||
"end_datetime": row[2],
|
||||
"start": {"latitude": row[3], "longitude": row[4]},
|
||||
"end": {"latitude": row[5], "longitude": row[6]},
|
||||
"distance_km": row[7],
|
||||
"duration_seconds": row[8],
|
||||
"point_count": row[9],
|
||||
"average_speed_kmh": row[10],
|
||||
"max_speed_kmh": row[11],
|
||||
} for row in rows]
|
||||
|
||||
|
||||
def get_trip_route(trip_id, max_points=2000):
|
||||
refresh_trips()
|
||||
conn = get_conn()
|
||||
if conn is None:
|
||||
return None
|
||||
with _conn_lock:
|
||||
trip = conn.execute(
|
||||
"""SELECT id, START_DATETIME, END_DATETIME, DISTANCE_KM,
|
||||
DURATION_SECONDS, POINT_COUNT,
|
||||
AVERAGE_SPEED_KMH, MAX_SPEED_KMH,
|
||||
START_TRACKING_ID, END_TRACKING_ID
|
||||
FROM trips WHERE id = ?""",
|
||||
(trip_id,),
|
||||
).fetchone()
|
||||
if trip is None:
|
||||
return None
|
||||
rows = conn.execute(
|
||||
"""SELECT id, DATETIME, GPSX, GPSY, SPEED
|
||||
FROM tracking WHERE id BETWEEN ? AND ?
|
||||
ORDER BY DATETIME ASC, id ASC""",
|
||||
(trip[8], trip[9]),
|
||||
).fetchall()
|
||||
return {
|
||||
"trip": {
|
||||
"id": trip[0],
|
||||
"start_datetime": trip[1],
|
||||
"end_datetime": trip[2],
|
||||
"distance_km": trip[3],
|
||||
"duration_seconds": trip[4],
|
||||
"point_count": trip[5],
|
||||
"average_speed_kmh": trip[6],
|
||||
"max_speed_kmh": trip[7],
|
||||
},
|
||||
"points": app_trip.route_points(rows, max_points=max_points),
|
||||
}
|
||||
|
||||
|
||||
def get_setting(key, default=None):
|
||||
@@ -307,13 +533,33 @@ def _log_loop():
|
||||
# (app_obd.py imports app_sql, and app_sql needs app_obd.speed here).
|
||||
from modules import app_obd
|
||||
|
||||
last_position = None
|
||||
last_logged_at = None
|
||||
while True:
|
||||
gps_x = app_gps.LocationX
|
||||
gps_y = app_gps.LocationY
|
||||
speed = app_obd.speed
|
||||
speed = app_obd.get_fresh_speed()
|
||||
|
||||
if gps_x is not None and gps_y is not None:
|
||||
insert_tracking(gps_x, gps_y, speed)
|
||||
try:
|
||||
position = (
|
||||
float(str(gps_x).replace("\x00", "").strip()),
|
||||
float(str(gps_y).replace("\x00", "").strip()),
|
||||
)
|
||||
except (TypeError, ValueError) as error:
|
||||
print("Skipping invalid GPS tracking snapshot:", error)
|
||||
else:
|
||||
now = time.monotonic()
|
||||
seconds_since_log = (
|
||||
now - last_logged_at
|
||||
if last_logged_at is not None
|
||||
else IDLE_LOG_INTERVAL
|
||||
)
|
||||
if should_log_tracking(
|
||||
position, speed, last_position, seconds_since_log
|
||||
) and insert_tracking(position[0], position[1], speed):
|
||||
last_position = position
|
||||
last_logged_at = now
|
||||
|
||||
time.sleep(LOG_INTERVAL)
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
INACTIVITY_SECONDS = 5 * 60
|
||||
GPS_MOTION_METERS = 8
|
||||
MIN_GPS_MOTION_SAMPLES = 3
|
||||
MAX_GPS_SAMPLE_GAP_SECONDS = 30
|
||||
MAX_PLAUSIBLE_SPEED_KMH = 220
|
||||
EARTH_RADIUS_KM = 6371.0088
|
||||
|
||||
|
||||
def _number(value):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(str(value).replace("\x00", "").strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _timestamp(value):
|
||||
try:
|
||||
return datetime.fromisoformat(str(value))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def haversine_km(left, right):
|
||||
lat1, lon1 = map(math.radians, left)
|
||||
lat2, lon2 = map(math.radians, right)
|
||||
latitude_delta = lat2 - lat1
|
||||
longitude_delta = lon2 - lon1
|
||||
value = (
|
||||
math.sin(latitude_delta / 2) ** 2
|
||||
+ math.cos(lat1) * math.cos(lat2)
|
||||
* math.sin(longitude_delta / 2) ** 2
|
||||
)
|
||||
return EARTH_RADIUS_KM * 2 * math.atan2(math.sqrt(value), math.sqrt(1 - value))
|
||||
|
||||
|
||||
def normalize_tracking_rows(rows):
|
||||
normalized = []
|
||||
for row in rows:
|
||||
timestamp = _timestamp(row[1])
|
||||
latitude = _number(row[2])
|
||||
longitude = _number(row[3])
|
||||
speed = _number(row[4]) or 0
|
||||
if timestamp is None or latitude is None or longitude is None:
|
||||
continue
|
||||
if not (-90 <= latitude <= 90 and -180 <= longitude <= 180):
|
||||
continue
|
||||
normalized.append({
|
||||
"id": int(row[0]),
|
||||
"datetime": timestamp,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"speed": max(0, speed),
|
||||
})
|
||||
return normalized
|
||||
|
||||
|
||||
def _segment(row, previous):
|
||||
seconds = (row["datetime"] - previous["datetime"]).total_seconds()
|
||||
if seconds <= 0:
|
||||
return 0, 0
|
||||
distance_km = haversine_km(
|
||||
(previous["latitude"], previous["longitude"]),
|
||||
(row["latitude"], row["longitude"]),
|
||||
)
|
||||
speed_kmh = distance_km / seconds * 3600
|
||||
if speed_kmh > MAX_PLAUSIBLE_SPEED_KMH:
|
||||
return 0, speed_kmh
|
||||
return distance_km, speed_kmh
|
||||
|
||||
|
||||
def build_trip_summaries(raw_rows):
|
||||
rows = normalize_tracking_rows(raw_rows)
|
||||
if len(rows) < 2:
|
||||
return []
|
||||
|
||||
obd_motion = []
|
||||
gps_motion = []
|
||||
for index in range(1, len(rows)):
|
||||
seconds = (
|
||||
rows[index]["datetime"] - rows[index - 1]["datetime"]
|
||||
).total_seconds()
|
||||
distance_km, _ = _segment(rows[index], rows[index - 1])
|
||||
has_obd_motion = rows[index]["speed"] > 1
|
||||
has_gps_motion = (
|
||||
0 < seconds <= MAX_GPS_SAMPLE_GAP_SECONDS
|
||||
and distance_km * 1000 >= GPS_MOTION_METERS
|
||||
)
|
||||
if has_obd_motion:
|
||||
obd_motion.append({
|
||||
"index": index,
|
||||
"obd": True,
|
||||
"distance_km": distance_km,
|
||||
})
|
||||
if has_gps_motion:
|
||||
gps_motion.append({
|
||||
"index": index,
|
||||
"obd": False,
|
||||
"distance_km": distance_km,
|
||||
})
|
||||
|
||||
qualified_gps_motion = []
|
||||
gps_sequences = []
|
||||
for event in gps_motion:
|
||||
if (
|
||||
not gps_sequences
|
||||
or (
|
||||
rows[event["index"]]["datetime"]
|
||||
- rows[gps_sequences[-1][-1]["index"]]["datetime"]
|
||||
).total_seconds() > MAX_GPS_SAMPLE_GAP_SECONDS
|
||||
):
|
||||
gps_sequences.append([event])
|
||||
else:
|
||||
gps_sequences[-1].append(event)
|
||||
for sequence in gps_sequences:
|
||||
if len(sequence) >= MIN_GPS_MOTION_SAMPLES:
|
||||
qualified_gps_motion.extend(sequence)
|
||||
|
||||
motion_by_index = {}
|
||||
for event in obd_motion + qualified_gps_motion:
|
||||
existing = motion_by_index.get(event["index"])
|
||||
if existing is None:
|
||||
motion_by_index[event["index"]] = event.copy()
|
||||
else:
|
||||
existing["obd"] = existing["obd"] or event["obd"]
|
||||
existing["distance_km"] = max(
|
||||
existing["distance_km"], event["distance_km"]
|
||||
)
|
||||
motion = [
|
||||
motion_by_index[index] for index in sorted(motion_by_index)
|
||||
]
|
||||
|
||||
groups = []
|
||||
for event in motion:
|
||||
event_gap = (
|
||||
(
|
||||
rows[event["index"]]["datetime"]
|
||||
- rows[groups[-1][-1]["index"]]["datetime"]
|
||||
).total_seconds()
|
||||
if groups
|
||||
else None
|
||||
)
|
||||
if (
|
||||
not groups
|
||||
or event_gap <= 0
|
||||
or event_gap > INACTIVITY_SECONDS
|
||||
):
|
||||
groups.append([event])
|
||||
else:
|
||||
groups[-1].append(event)
|
||||
|
||||
trips = []
|
||||
for events in groups:
|
||||
if not any(event["obd"] for event in events) and len(events) < MIN_GPS_MOTION_SAMPLES:
|
||||
continue
|
||||
|
||||
start_index = max(0, events[0]["index"] - 1)
|
||||
end_index = events[-1]["index"]
|
||||
trip_rows = rows[start_index:end_index + 1]
|
||||
distance_km = 0
|
||||
segment_speeds = []
|
||||
obd_speeds = []
|
||||
has_obd_motion = any(event["obd"] for event in events)
|
||||
for previous, row in zip(trip_rows, trip_rows[1:]):
|
||||
segment_km, segment_speed = _segment(row, previous)
|
||||
minimum_distance_meters = 1 if has_obd_motion else GPS_MOTION_METERS
|
||||
if segment_km * 1000 >= minimum_distance_meters:
|
||||
distance_km += segment_km
|
||||
segment_speeds.append(segment_speed)
|
||||
if row["speed"] > 0:
|
||||
obd_speeds.append(row["speed"])
|
||||
|
||||
start = trip_rows[0]
|
||||
end = trip_rows[-1]
|
||||
duration_seconds = max(
|
||||
0, int((end["datetime"] - start["datetime"]).total_seconds())
|
||||
)
|
||||
if not any(event["obd"] for event in events) and duration_seconds < 10:
|
||||
continue
|
||||
|
||||
trips.append({
|
||||
"id": start["id"],
|
||||
"start_tracking_id": start["id"],
|
||||
"end_tracking_id": end["id"],
|
||||
"start_datetime": start["datetime"].isoformat(sep=" "),
|
||||
"end_datetime": end["datetime"].isoformat(sep=" "),
|
||||
"start_gpsx": start["latitude"],
|
||||
"start_gpsy": start["longitude"],
|
||||
"end_gpsx": end["latitude"],
|
||||
"end_gpsy": end["longitude"],
|
||||
"distance_km": round(distance_km, 3),
|
||||
"duration_seconds": duration_seconds,
|
||||
"point_count": len(trip_rows),
|
||||
"average_speed_kmh": round(
|
||||
distance_km / duration_seconds * 3600, 1
|
||||
) if duration_seconds else 0,
|
||||
"max_speed_kmh": round(
|
||||
max(obd_speeds or segment_speeds or [0]), 1
|
||||
),
|
||||
})
|
||||
return trips
|
||||
|
||||
|
||||
def route_points(raw_rows, max_points=2000):
|
||||
rows = normalize_tracking_rows(raw_rows)
|
||||
filtered_rows = []
|
||||
for row in rows:
|
||||
if filtered_rows:
|
||||
_, segment_speed = _segment(row, filtered_rows[-1])
|
||||
if segment_speed > MAX_PLAUSIBLE_SPEED_KMH:
|
||||
continue
|
||||
filtered_rows.append(row)
|
||||
rows = filtered_rows
|
||||
for index, row in enumerate(rows):
|
||||
row["speed_source"] = "obd" if row["speed"] > 0 else "gps"
|
||||
if row["speed"] <= 0 and index:
|
||||
distance_km, segment_speed = _segment(row, rows[index - 1])
|
||||
row["speed"] = (
|
||||
segment_speed
|
||||
if distance_km * 1000 >= GPS_MOTION_METERS
|
||||
else 0
|
||||
)
|
||||
if len(rows) <= max_points:
|
||||
selected = rows
|
||||
else:
|
||||
indexes = {
|
||||
round(index * (len(rows) - 1) / (max_points - 1))
|
||||
for index in range(max_points)
|
||||
}
|
||||
selected = [row for index, row in enumerate(rows) if index in indexes]
|
||||
return [{
|
||||
"id": row["id"],
|
||||
"datetime": row["datetime"].isoformat(sep=" "),
|
||||
"latitude": row["latitude"],
|
||||
"longitude": row["longitude"],
|
||||
"speed": round(row["speed"], 1),
|
||||
"speed_source": row["speed_source"],
|
||||
} for row in selected]
|
||||
Reference in New Issue
Block a user