| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- from __future__ import annotations
- import os
- import sqlite3
- import threading
- import time
- from dataclasses import dataclass
- from pathlib import Path
- from typing import List, Optional
- @dataclass(frozen=True)
- class RouteInfo:
- id: int
- name: str
- mode: str
- created_at: int
- finished_at: Optional[int]
- points: int
- hazards: int
- active: bool
- monitoring: bool
- @dataclass(frozen=True)
- class RoutePoint:
- seq: int
- t: int
- lat: float
- lon: float
- speed_mps: float
- cum_dist_m: float
- counter_value: int = 0
- counter_absolute: int = 0
- @dataclass(frozen=True)
- class Hazard:
- id: int
- route_id: int
- seq: int
- t: int
- lat: float
- lon: float
- cum_dist_m: float
- type: str
- note: str
- counter_value: int = 0
- counter_absolute: int = 0
- class NavDB:
- def __init__(self, db_path: str):
- self.db_path = db_path
- os.makedirs(os.path.dirname(db_path), exist_ok=True)
- self._lock = threading.Lock()
- self._conn = sqlite3.connect(db_path, check_same_thread=False)
- self._conn.row_factory = sqlite3.Row
- self._init_schema()
- @classmethod
- def default(cls) -> "NavDB":
- base = Path(__file__).resolve().parents[2]
- return cls(str(base / "data" / "nav_copilot.sqlite3"))
- def _init_schema(self) -> None:
- with self._lock:
- cur = self._conn.cursor()
- cur.execute(
- """
- CREATE TABLE IF NOT EXISTS routes (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- mode TEXT NOT NULL DEFAULT 'gps',
- created_at INTEGER NOT NULL,
- finished_at INTEGER,
- active INTEGER NOT NULL DEFAULT 0,
- monitoring INTEGER NOT NULL DEFAULT 0
- )
- """
- )
- cur.execute(
- """
- CREATE TABLE IF NOT EXISTS route_points (
- route_id INTEGER NOT NULL,
- seq INTEGER NOT NULL,
- t INTEGER NOT NULL,
- lat REAL NOT NULL,
- lon REAL NOT NULL,
- speed_mps REAL NOT NULL,
- cum_dist_m REAL NOT NULL,
- counter_value INTEGER NOT NULL DEFAULT 0,
- counter_absolute INTEGER NOT NULL DEFAULT 0,
- PRIMARY KEY(route_id, seq)
- )
- """
- )
- cur.execute(
- """
- CREATE TABLE IF NOT EXISTS hazards (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- route_id INTEGER NOT NULL,
- seq INTEGER NOT NULL,
- t INTEGER NOT NULL,
- lat REAL NOT NULL,
- lon REAL NOT NULL,
- cum_dist_m REAL NOT NULL,
- type TEXT NOT NULL,
- note TEXT NOT NULL DEFAULT '',
- counter_value INTEGER NOT NULL DEFAULT 0,
- counter_absolute INTEGER NOT NULL DEFAULT 0
- )
- """
- )
- self._ensure_column(cur, "routes", "mode", "TEXT NOT NULL DEFAULT 'gps'")
- self._ensure_column(
- cur,
- "routes",
- "monitoring",
- "INTEGER NOT NULL DEFAULT 0",
- )
- self._ensure_column(
- cur,
- "route_points",
- "counter_value",
- "INTEGER NOT NULL DEFAULT 0",
- )
- self._ensure_column(
- cur,
- "route_points",
- "counter_absolute",
- "INTEGER NOT NULL DEFAULT 0",
- )
- self._ensure_column(
- cur,
- "hazards",
- "counter_value",
- "INTEGER NOT NULL DEFAULT 0",
- )
- self._ensure_column(
- cur,
- "hazards",
- "counter_absolute",
- "INTEGER NOT NULL DEFAULT 0",
- )
- self._conn.commit()
- def _ensure_column(
- self,
- cur: sqlite3.Cursor,
- table_name: str,
- column_name: str,
- column_ddl: str,
- ) -> None:
- columns = {
- str(row["name"])
- for row in cur.execute(f"PRAGMA table_info({table_name})").fetchall()
- }
- if column_name not in columns:
- cur.execute(
- f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_ddl}"
- )
- def create_route(self, name: str, mode: str = "gps") -> int:
- now = int(time.time())
- route_mode = (mode or "gps").strip() or "gps"
- with self._lock:
- self._conn.execute("UPDATE routes SET active=0, monitoring=0")
- cur = self._conn.execute(
- "INSERT INTO routes(name, mode, created_at, active, monitoring) VALUES(?, ?, ?, 1, 0)",
- (name, route_mode, now),
- )
- self._conn.commit()
- return int(cur.lastrowid)
- def finish_route(self, route_id: int) -> None:
- now = int(time.time())
- with self._lock:
- self._conn.execute(
- "UPDATE routes SET finished_at=? WHERE id=?",
- (now, route_id),
- )
- self._conn.commit()
- def set_active_route(self, route_id: int) -> None:
- with self._lock:
- self._conn.execute("UPDATE routes SET active=0")
- self._conn.execute("UPDATE routes SET active=1 WHERE id=?", (route_id,))
- self._conn.commit()
- def get_active_route_id(self) -> Optional[int]:
- with self._lock:
- row = self._conn.execute(
- "SELECT id FROM routes WHERE active=1 ORDER BY id DESC LIMIT 1"
- ).fetchone()
- return int(row["id"]) if row else None
- def set_monitoring_route(self, route_id: Optional[int]) -> None:
- with self._lock:
- self._conn.execute("UPDATE routes SET monitoring=0")
- if route_id is not None:
- self._conn.execute(
- "UPDATE routes SET monitoring=1 WHERE id=?",
- (int(route_id),),
- )
- self._conn.commit()
- def get_monitoring_route_id(self) -> Optional[int]:
- with self._lock:
- row = self._conn.execute(
- "SELECT id FROM routes WHERE monitoring=1 ORDER BY id DESC LIMIT 1"
- ).fetchone()
- return int(row["id"]) if row else None
- # Backward-compatible aliases for older call sites that used sanitized names.
- def setmonitoringroute(self, route_id: Optional[int]) -> None:
- self.set_monitoring_route(route_id)
- def getmonitoringrouteid(self) -> Optional[int]:
- return self.get_monitoring_route_id()
- def get_route_mode(self, route_id: int) -> str:
- with self._lock:
- row = self._conn.execute(
- "SELECT mode FROM routes WHERE id=?",
- (route_id,),
- ).fetchone()
- if not row:
- return "gps"
- return str(row["mode"] or "gps")
- def update_route_mode(self, route_id: int, mode: str) -> None:
- route_mode = (mode or "gps").strip() or "gps"
- with self._lock:
- self._conn.execute(
- "UPDATE routes SET mode=? WHERE id=?",
- (route_mode, route_id),
- )
- self._conn.commit()
- def add_point(
- self,
- *,
- route_id: int,
- seq: int,
- t: int,
- lat: float,
- lon: float,
- speed_mps: float,
- cum_dist_m: float,
- counter_value: int = 0,
- counter_absolute: int = 0,
- ) -> None:
- with self._lock:
- self._conn.execute(
- """
- INSERT OR REPLACE INTO route_points(
- route_id, seq, t, lat, lon, speed_mps, cum_dist_m, counter_value, counter_absolute
- )
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- route_id,
- seq,
- t,
- lat,
- lon,
- speed_mps,
- cum_dist_m,
- int(counter_value or 0),
- int(counter_absolute or 0),
- ),
- )
- self._conn.commit()
- def add_hazard(
- self,
- *,
- route_id: int,
- seq: int,
- t: int,
- lat: float,
- lon: float,
- cum_dist_m: float,
- hazard_type: str,
- note: str = "",
- counter_value: int = 0,
- counter_absolute: int = 0,
- ) -> int:
- with self._lock:
- cur = self._conn.execute(
- """
- INSERT INTO hazards(
- route_id, seq, t, lat, lon, cum_dist_m, type, note, counter_value, counter_absolute
- )
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- route_id,
- seq,
- t,
- lat,
- lon,
- cum_dist_m,
- hazard_type,
- note or "",
- int(counter_value or 0),
- int(counter_absolute or 0),
- ),
- )
- self._conn.commit()
- return int(cur.lastrowid)
- def list_routes(self, limit: int = 20) -> List[RouteInfo]:
- with self._lock:
- rows = self._conn.execute(
- """
- SELECT
- r.id, r.name, r.mode, r.created_at, r.finished_at, r.active, r.monitoring,
- (SELECT COUNT(1) FROM route_points p WHERE p.route_id=r.id) AS points,
- (SELECT COUNT(1) FROM hazards h WHERE h.route_id=r.id) AS hazards
- FROM routes r
- ORDER BY r.id DESC
- LIMIT ?
- """,
- (int(limit),),
- ).fetchall()
- return [
- RouteInfo(
- id=int(row["id"]),
- name=str(row["name"]),
- mode=str(row["mode"] or "gps"),
- created_at=int(row["created_at"]),
- finished_at=int(row["finished_at"]) if row["finished_at"] else None,
- points=int(row["points"]),
- hazards=int(row["hazards"]),
- active=bool(int(row["active"])),
- monitoring=bool(int(row["monitoring"] or 0)),
- )
- for row in rows
- ]
- def load_route_points(self, route_id: int) -> List[RoutePoint]:
- with self._lock:
- rows = self._conn.execute(
- """
- SELECT seq, t, lat, lon, speed_mps, cum_dist_m, counter_value, counter_absolute
- FROM route_points
- WHERE route_id=?
- ORDER BY seq ASC
- """,
- (route_id,),
- ).fetchall()
- return [
- RoutePoint(
- seq=int(row["seq"]),
- t=int(row["t"]),
- lat=float(row["lat"]),
- lon=float(row["lon"]),
- speed_mps=float(row["speed_mps"]),
- cum_dist_m=float(row["cum_dist_m"]),
- counter_value=int(row["counter_value"] or 0),
- counter_absolute=int(row["counter_absolute"] or 0),
- )
- for row in rows
- ]
- def load_hazards(self, route_id: int) -> List[Hazard]:
- with self._lock:
- rows = self._conn.execute(
- """
- SELECT
- id, route_id, seq, t, lat, lon, cum_dist_m, type, note, counter_value, counter_absolute
- FROM hazards
- WHERE route_id=?
- ORDER BY cum_dist_m ASC, id ASC
- """,
- (route_id,),
- ).fetchall()
- return [
- Hazard(
- id=int(row["id"]),
- route_id=int(row["route_id"]),
- seq=int(row["seq"]),
- t=int(row["t"]),
- lat=float(row["lat"]),
- lon=float(row["lon"]),
- cum_dist_m=float(row["cum_dist_m"]),
- type=str(row["type"]),
- note=str(row["note"] or ""),
- counter_value=int(row["counter_value"] or 0),
- counter_absolute=int(row["counter_absolute"] or 0),
- )
- for row in rows
- ]
|