perf(viewer): polylines USV <3s — Phase 1 usv_track endpoint + immediate applyTrailAndCursor
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import csv
|
||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
@@ -138,3 +140,76 @@ async def get_tracks(sortie_id: str):
|
|||||||
raise HTTPException(404, "tracks.geojson not found")
|
raise HTTPException(404, "tracks.geojson not found")
|
||||||
with open(p) as f:
|
with open(p) as f:
|
||||||
return JSONResponse(json.load(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)
|
||||||
|
|||||||
@@ -1280,8 +1280,42 @@ function setupSyncedZoom(chartIds) {
|
|||||||
async function loadSortieData(sortieId) {
|
async function loadSortieData(sortieId) {
|
||||||
const prog = document.getElementById('sync-progress');
|
const prog = document.getElementById('sync-progress');
|
||||||
try {
|
try {
|
||||||
prog.textContent = 'Chargement USV…';
|
// Reset state for sortie mode (independent from datebar)
|
||||||
const usvResp = await fetch(`${API2}/sorties/${encodeURIComponent(sortieId)}/usv`);
|
clearMapLayers();
|
||||||
|
allPoints = [];
|
||||||
|
usblPoints = [];
|
||||||
|
|
||||||
|
// Phase 1: USV GPS track → polylines on map immediately (<3s target)
|
||||||
|
prog.textContent = 'Chargement track USV…';
|
||||||
|
const trackResp = await fetch(`${API2}/sorties/${encodeURIComponent(sortieId)}/usv_track`,
|
||||||
|
{ signal: AbortSignal.timeout(15000) });
|
||||||
|
if (trackResp.ok) {
|
||||||
|
const trackPts = await trackResp.json();
|
||||||
|
allPoints.push(...trackPts);
|
||||||
|
allPoints.sort((a, b) => a.t_ms - b.t_ms);
|
||||||
|
if (allPoints.length > 0) {
|
||||||
|
const times = allPoints.map(p => p.t_ms);
|
||||||
|
tMin = Math.min(...times);
|
||||||
|
tMax = Math.max(...times);
|
||||||
|
tNow = tMax;
|
||||||
|
// Fit map bounds
|
||||||
|
const lats = allPoints.map(p => p.lat).filter(Boolean);
|
||||||
|
const lons = allPoints.map(p => p.lon).filter(Boolean);
|
||||||
|
if (lats.length) {
|
||||||
|
map.fitBounds([
|
||||||
|
[Math.min(...lats), Math.min(...lons)],
|
||||||
|
[Math.max(...lats), Math.max(...lons)],
|
||||||
|
], { padding: [40, 40] });
|
||||||
|
}
|
||||||
|
showNoDataOverlay(false);
|
||||||
|
applyTrailAndCursor(); // PERF FIX: polylines visible NOW, before charts
|
||||||
|
prog.textContent = `Track USV ${allPoints.length} pts — chargement charts…`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: series + AUV (charts populate progressively)
|
||||||
|
const usvResp = await fetch(`${API2}/sorties/${encodeURIComponent(sortieId)}/usv`,
|
||||||
|
{ signal: AbortSignal.timeout(20000) });
|
||||||
if (usvResp.ok) {
|
if (usvResp.ok) {
|
||||||
const usvData = await usvResp.json();
|
const usvData = await usvResp.json();
|
||||||
switchTab('usv'); // BUG1d FIX: switch BEFORE renderUSV so Plotly renders on visible divs
|
switchTab('usv'); // BUG1d FIX: switch BEFORE renderUSV so Plotly renders on visible divs
|
||||||
@@ -1291,7 +1325,7 @@ async function loadSortieData(sortieId) {
|
|||||||
prog.textContent = 'Chargement AUV…';
|
prog.textContent = 'Chargement AUV…';
|
||||||
await loadAuvTabs(sortieId);
|
await loadAuvTabs(sortieId);
|
||||||
prog.textContent = `${sortieId} chargé`;
|
prog.textContent = `${sortieId} chargé`;
|
||||||
// POLYLINE FIX: ensure map trail is drawn if allPoints already loaded from datebar
|
// Re-apply after AUV usbl_track loaded (adds AUV trail if available)
|
||||||
if (allPoints.length > 0) applyTrailAndCursor();
|
if (allPoints.length > 0) applyTrailAndCursor();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
prog.textContent = `Erreur: ${e.message}`;
|
prog.textContent = `Erreur: ${e.message}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user