perf(viewer): polylines USV <3s — Phase 1 usv_track endpoint + immediate applyTrailAndCursor

This commit is contained in:
Poulpe
2026-04-28 15:03:53 +00:00
parent f5788a01f4
commit dd6f0cf435
2 changed files with 112 additions and 3 deletions

View File

@@ -1,7 +1,9 @@
import asyncio
import csv
import gzip
import json
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import AsyncGenerator
@@ -138,3 +140,76 @@ async def get_tracks(sortie_id: str):
raise HTTPException(404, "tracks.geojson not found")
with open(p) as f:
return JSONResponse(json.load(f))
def _ts_nav(ts_str: str) -> float:
for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S"):
try:
return datetime.strptime(ts_str.strip(), fmt).replace(tzinfo=timezone.utc).timestamp()
except ValueError:
continue
return 0.0
def _read_usv_track(nav_log_path: Path, max_pts: int = 2000) -> list[dict]:
"""Read navigation_log.csv → [{t_ms, lat, lon, heading, source}] downsampled."""
pts: list[dict] = []
lat_map: dict[float, float] = {}
lon_map: dict[float, float] = {}
heading_map: dict[float, float] = {}
with open(nav_log_path, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
next(reader, None) # skip header
for row in reader:
if len(row) < 3:
continue
ts_str, field, val = row[0], row[1], row[2]
if field not in ("Lat", "Lon", "Heading"):
continue
t = _ts_nav(ts_str)
try:
v = float(val)
except ValueError:
continue
if field == "Lat":
lat_map[t] = v
elif field == "Lon":
lon_map[t] = v
else:
heading_map[t] = v
# Join on lat timestamps (master)
source = nav_log_path.parent.name
for t, lat in sorted(lat_map.items()):
lon = lon_map.get(t)
if lon is None:
# nearest lon within 1s
near = min(lon_map.keys(), key=lambda x: abs(x - t), default=None)
if near is None or abs(near - t) > 1.0:
continue
lon = lon_map[near]
pts.append({
"t_ms": int(t * 1000),
"lat": lat,
"lon": lon,
"heading": heading_map.get(t),
"source": source,
})
# Simple stride downsampling
if len(pts) > max_pts:
step = len(pts) // max_pts
pts = pts[::step]
return pts
@app.get("/sorties/{sortie_id:path}/usv_track")
async def get_usv_track(sortie_id: str):
"""Return USV GPS track [{t_ms, lat, lon, heading, source}] for map polylines."""
raw_dir = OUTPUT_DIR / sortie_id / "raw"
nav_logs = list(raw_dir.rglob("*_navigation_log.csv")) if raw_dir.exists() else []
if not nav_logs:
raise HTTPException(404, "No navigation_log.csv found — run pipeline first")
pts: list[dict] = []
for nav_log in nav_logs:
pts.extend(await asyncio.to_thread(_read_usv_track, nav_log))
pts.sort(key=lambda p: p["t_ms"])
return JSONResponse(pts)