Alex got his trails
This commit is contained in:
parent
0627dfb29a
commit
66ed711fec
5 changed files with 90 additions and 4 deletions
|
|
@ -30,7 +30,7 @@ This project provides:
|
||||||
- **GET /history**: Retrieve historical telemetry data with optional time filtering.
|
- **GET /history**: Retrieve historical telemetry data with optional time filtering.
|
||||||
- **GET /debug**: Health check endpoint.
|
- **GET /debug**: Health check endpoint.
|
||||||
- **Live Map**: Interactive map interface with panning, zooming, and sorting.
|
- **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
|
## Requirements
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ Response:
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
- **Live Map**: `static/index.html` – Real-time player positions on a map.
|
- **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
|
## Database Schema
|
||||||
|
|
||||||
|
|
|
||||||
37
main.py
37
main.py
|
|
@ -151,6 +151,43 @@ def get_history(
|
||||||
]
|
]
|
||||||
return JSONResponse(content={"data": data})
|
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 ---------------------------
|
# -------------------- static frontend ---------------------------
|
||||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<div id="mapContainer">
|
<div id="mapContainer">
|
||||||
<div id="mapGroup">
|
<div id="mapGroup">
|
||||||
<img id="map" src="dereth.png" alt="Dereth map">
|
<img id="map" src="dereth.png" alt="Dereth map">
|
||||||
|
<svg id="trails"></svg>
|
||||||
<div id="dots"></div>
|
<div id="dots"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tooltip" class="tooltip"></div>
|
<div id="tooltip" class="tooltip"></div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const wrap = document.getElementById('mapContainer');
|
||||||
const group = document.getElementById('mapGroup');
|
const group = document.getElementById('mapGroup');
|
||||||
const img = document.getElementById('map');
|
const img = document.getElementById('map');
|
||||||
const dots = document.getElementById('dots');
|
const dots = document.getElementById('dots');
|
||||||
|
const trailsContainer = document.getElementById('trails');
|
||||||
const list = document.getElementById('playerList');
|
const list = document.getElementById('playerList');
|
||||||
const btnContainer = document.getElementById('sortButtons');
|
const btnContainer = document.getElementById('sortButtons');
|
||||||
const tooltip = document.getElementById('tooltip');
|
const tooltip = document.getElementById('tooltip');
|
||||||
|
|
@ -130,11 +131,17 @@ function hideTooltip() {
|
||||||
/* ---------- polling and initialization -------------------------- */
|
/* ---------- polling and initialization -------------------------- */
|
||||||
async function pollLive() {
|
async function pollLive() {
|
||||||
try {
|
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;
|
currentPlayers = players;
|
||||||
|
renderTrails(trails);
|
||||||
renderList();
|
renderList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Live fetch failed:', e);
|
console.error('Live or trails fetch failed:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,6 +154,12 @@ function startPolling() {
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
imgW = img.naturalWidth;
|
imgW = img.naturalWidth;
|
||||||
imgH = img.naturalHeight;
|
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();
|
fitToWindow();
|
||||||
startPolling();
|
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 ------------ */
|
/* ---------- selection centering, focus zoom & blink ------------ */
|
||||||
function selectPlayer(p, x, y) {
|
function selectPlayer(p, x, y) {
|
||||||
selected = p.character_name;
|
selected = p.character_name;
|
||||||
|
|
|
||||||
|
|
@ -202,3 +202,17 @@ body {
|
||||||
/* hover & selected states */
|
/* hover & selected states */
|
||||||
#playerList li:hover { background: var(--card-hov); }
|
#playerList li:hover { background: var(--card-hov); }
|
||||||
#playerList li.selected { background: #454545; }
|
#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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue