| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- 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 LineAlert:
- id: int
- device_id: str
- start_lat: float
- start_lon: float
- end_lat: float
- end_lon: float
- coord_type: str
- note: str
- created_at: int
- class NavDB:
- _default_instance: Optional["NavDB"] = None
- _default_lock = threading.Lock()
- 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":
- if cls._default_instance is None:
- with cls._default_lock:
- if cls._default_instance is None:
- base = Path(__file__).resolve().parents[2]
- cls._default_instance = cls(str(base / "data" / "nav_copilot.sqlite3"))
- return cls._default_instance
- def _init_schema(self) -> None:
- with self._lock:
- cur = self._conn.cursor()
- # The old route/hazard model is no longer used. Drop it so the
- # SQLite file only contains app-managed line alerts.
- cur.execute("DROP TABLE IF EXISTS route_points")
- cur.execute("DROP TABLE IF EXISTS hazards")
- cur.execute("DROP TABLE IF EXISTS routes")
- cur.execute(
- """
- CREATE TABLE IF NOT EXISTS line_alerts (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- device_id TEXT NOT NULL,
- start_lat REAL NOT NULL,
- start_lon REAL NOT NULL,
- end_lat REAL NOT NULL,
- end_lon REAL NOT NULL,
- coord_type TEXT NOT NULL DEFAULT 'BD09',
- note TEXT NOT NULL,
- created_at INTEGER NOT NULL
- )
- """
- )
- self._ensure_column(
- cur,
- "line_alerts",
- "coord_type",
- "TEXT NOT NULL DEFAULT 'BD09'",
- )
- cur.execute(
- """
- CREATE INDEX IF NOT EXISTS idx_line_alerts_device_id
- ON line_alerts(device_id)
- """
- )
- 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 count_line_alerts(self, device_id: str) -> int:
- with self._lock:
- row = self._conn.execute(
- "SELECT COUNT(1) AS total FROM line_alerts WHERE device_id=?",
- (device_id,),
- ).fetchone()
- return int(row["total"] or 0) if row else 0
- def get_line_alert(self, alert_id: int, device_id: Optional[str] = None) -> Optional[LineAlert]:
- query = """
- SELECT id, device_id, start_lat, start_lon, end_lat, end_lon, coord_type, note, created_at
- FROM line_alerts
- WHERE id=?
- """
- params: list[object] = [int(alert_id)]
- if device_id is not None:
- query += " AND device_id=?"
- params.append(device_id)
- with self._lock:
- row = self._conn.execute(query, tuple(params)).fetchone()
- return self._row_to_line_alert(row) if row else None
- def list_line_alerts(self, device_id: str) -> List[LineAlert]:
- with self._lock:
- rows = self._conn.execute(
- """
- SELECT id, device_id, start_lat, start_lon, end_lat, end_lon, coord_type, note, created_at
- FROM line_alerts
- WHERE device_id=?
- ORDER BY id DESC
- """,
- (device_id,),
- ).fetchall()
- return [self._row_to_line_alert(row) for row in rows]
- def create_line_alert(
- self,
- *,
- device_id: str,
- start_lat: float,
- start_lon: float,
- end_lat: float,
- end_lon: float,
- coord_type: str,
- note: str,
- ) -> int:
- now = int(time.time())
- with self._lock:
- cur = self._conn.execute(
- """
- INSERT INTO line_alerts(
- device_id, start_lat, start_lon, end_lat, end_lon, coord_type, note, created_at
- )
- VALUES(?, ?, ?, ?, ?, ?, ?, ?)
- """,
- (
- device_id,
- float(start_lat),
- float(start_lon),
- float(end_lat),
- float(end_lon),
- coord_type,
- note,
- now,
- ),
- )
- self._conn.commit()
- return int(cur.lastrowid)
- def delete_line_alert(self, alert_id: int, device_id: Optional[str] = None) -> bool:
- query = "DELETE FROM line_alerts WHERE id=?"
- params: list[object] = [int(alert_id)]
- if device_id is not None:
- query += " AND device_id=?"
- params.append(device_id)
- with self._lock:
- cur = self._conn.execute(query, tuple(params))
- self._conn.commit()
- return int(cur.rowcount or 0) > 0
- @staticmethod
- def _row_to_line_alert(row: sqlite3.Row) -> LineAlert:
- return LineAlert(
- id=int(row["id"]),
- device_id=str(row["device_id"]),
- start_lat=float(row["start_lat"]),
- start_lon=float(row["start_lon"]),
- end_lat=float(row["end_lat"]),
- end_lon=float(row["end_lon"]),
- coord_type=str(row["coord_type"] or "BD09"),
- note=str(row["note"] or ""),
- created_at=int(row["created_at"]),
- )
|