From 66ed711fec1e7b9a351bcd8284ff6631d97997b8 Mon Sep 17 00:00:00 2001 From: erik Date: Wed, 30 Apr 2025 22:04:06 +0000 Subject: [PATCH] Alex got his trails --- README.md | 4 ++-- main.py | 37 +++++++++++++++++++++++++++++++++++++ static/index.html | 1 + static/script.js | 38 ++++++++++++++++++++++++++++++++++++-- static/style.css | 14 ++++++++++++++ 5 files changed, 90 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0082d0a4..a4d1aaca 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This project provides: - **GET /history**: Retrieve historical telemetry data with optional time filtering. - **GET /debug**: Health check endpoint. - **Live Map**: Interactive map interface with panning, zooming, and sorting. -- **Analytics Dashboard**: Charts for kills over time and kills per hour. + - **Analytics Dashboard**: Interactive charts for kills over time and kills per hour using D3.js. ## Requirements @@ -131,7 +131,7 @@ Response: ## Frontend - **Live Map**: `static/index.html` – Real-time player positions on a map. -- **Analytics**: `static/graphs.html` – Charts powered by [Chart.js](https://www.chartjs.org/). +- **Analytics Dashboard**: `static/graphs.html` – Interactive charts powered by [D3.js](https://d3js.org/). ## Database Schema diff --git a/main.py b/main.py index d1995c4c..eb841192 100644 --- a/main.py +++ b/main.py @@ -151,6 +151,43 @@ def get_history( ] return JSONResponse(content={"data": data}) +# ------------------------ GET Trails --------------------------------- +@app.get("/trails") +@app.get("/trails/") +def get_trails( + seconds: int = Query(600, ge=0, description="Lookback window in seconds") +): + """ + Return position snapshots (timestamp, character_name, ew, ns, z) + for the past `seconds` seconds. + """ + # match the same string format as stored timestamps (via str(datetime)) + cutoff_dt = datetime.utcnow().replace(tzinfo=timezone.utc) - timedelta(seconds=seconds) + cutoff = str(cutoff_dt) + conn = sqlite3.connect(DB_FILE) + conn.row_factory = sqlite3.Row + rows = conn.execute( + """ + SELECT timestamp, character_name, ew, ns, z + FROM telemetry_log + WHERE timestamp >= ? + ORDER BY character_name, timestamp + """, + (cutoff,) + ).fetchall() + conn.close() + trails = [ + { + "timestamp": r["timestamp"], + "character_name": r["character_name"], + "ew": r["ew"], + "ns": r["ns"], + "z": r["z"], + } + for r in rows + ] + return JSONResponse(content={"trails": trails}) + # -------------------- static frontend --------------------------- app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/static/index.html b/static/index.html index 66e6da0f..94dc2ed3 100644 --- a/static/index.html +++ b/static/index.html @@ -20,6 +20,7 @@
Dereth map +
diff --git a/static/script.js b/static/script.js index ba7462e6..0b6b9aff 100644 --- a/static/script.js +++ b/static/script.js @@ -3,6 +3,7 @@ const wrap = document.getElementById('mapContainer'); const group = document.getElementById('mapGroup'); const img = document.getElementById('map'); const dots = document.getElementById('dots'); +const trailsContainer = document.getElementById('trails'); const list = document.getElementById('playerList'); const btnContainer = document.getElementById('sortButtons'); const tooltip = document.getElementById('tooltip'); @@ -130,11 +131,17 @@ function hideTooltip() { /* ---------- polling and initialization -------------------------- */ async function pollLive() { try { - const { players } = await (await fetch('/live/')).json(); + const [liveRes, trailsRes] = await Promise.all([ + fetch('/live/'), + fetch('/trails/?seconds=600'), + ]); + const { players } = await liveRes.json(); + const { trails } = await trailsRes.json(); currentPlayers = players; + renderTrails(trails); renderList(); } catch (e) { - console.error('Live fetch failed:', e); + console.error('Live or trails fetch failed:', e); } } @@ -147,6 +154,12 @@ function startPolling() { img.onload = () => { imgW = img.naturalWidth; imgH = img.naturalHeight; + // size the SVG trails container to match the map dimensions + if (trailsContainer) { + trailsContainer.setAttribute('viewBox', `0 0 ${imgW} ${imgH}`); + trailsContainer.setAttribute('width', `${imgW}`); + trailsContainer.setAttribute('height', `${imgH}`); + } fitToWindow(); startPolling(); }; @@ -204,6 +217,27 @@ function render(players) { }); } +/* ---------- rendering trails ------------------------------- */ +function renderTrails(trailData) { + trailsContainer.innerHTML = ''; + const byChar = trailData.reduce((acc, pt) => { + (acc[pt.character_name] = acc[pt.character_name] || []).push(pt); + return acc; + }, {}); + for (const [name, pts] of Object.entries(byChar)) { + if (pts.length < 2) continue; + const points = pts.map(pt => { + const { x, y } = worldToPx(pt.ew, pt.ns); + return `${x},${y}`; + }).join(' '); + const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); + poly.setAttribute('points', points); + poly.setAttribute('stroke', hue(name)); + poly.setAttribute('fill', 'none'); + poly.setAttribute('class', 'trail-path'); + trailsContainer.appendChild(poly); + } +} /* ---------- selection centering, focus zoom & blink ------------ */ function selectPlayer(p, x, y) { selected = p.character_name; diff --git a/static/style.css b/static/style.css index 25173869..031a41be 100644 --- a/static/style.css +++ b/static/style.css @@ -202,3 +202,17 @@ body { /* hover & selected states */ #playerList li:hover { background: var(--card-hov); } #playerList li.selected { background: #454545; } +/* trails paths */ +#trails { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} +.trail-path { + fill: none; + stroke-width: 2px; + stroke-opacity: 0.7; + stroke-linecap: round; + stroke-linejoin: round; +}