144 lines
5.4 KiB
JavaScript
144 lines
5.4 KiB
JavaScript
const PRACTICE_TYPES = new Set(["open_practice", "free_practice", "practice", "qualification"]);
|
|
|
|
export function normalizeSessionReplayPassings(passings) {
|
|
return (Array.isArray(passings) ? passings : [])
|
|
.filter((passing) => Number.isFinite(Number(passing?.timestamp)))
|
|
.map((passing, index) => ({
|
|
...passing,
|
|
replayIndex: index,
|
|
timestamp: Number(passing.timestamp),
|
|
lapMs: Math.max(0, Number(passing.lapMs || 0) || 0),
|
|
transponder: String(passing.transponder || ""),
|
|
competitorKey: String(passing.competitorKey || passing.driverId || passing.teamId || passing.transponder || `passing_${index}`),
|
|
driverName: String(passing.displayName || passing.teamName || passing.driverName || passing.transponder || "Unknown"),
|
|
carName: String(passing.subLabel || passing.carName || ""),
|
|
}))
|
|
.sort((left, right) => left.timestamp - right.timestamp || left.replayIndex - right.replayIndex);
|
|
}
|
|
|
|
export function createSessionReplayRuntime() {
|
|
return {
|
|
sessionId: "",
|
|
sessionType: "",
|
|
sessionName: "",
|
|
status: "idle",
|
|
speed: 1,
|
|
elapsedMs: 0,
|
|
durationMs: 0,
|
|
startedWallAt: 0,
|
|
startedElapsedMs: 0,
|
|
passings: [],
|
|
timer: null,
|
|
};
|
|
}
|
|
|
|
export function loadSessionReplay(runtime, session, result) {
|
|
const passings = normalizeSessionReplayPassings(result?.passings);
|
|
runtime.sessionId = String(session?.id || "");
|
|
runtime.sessionType = String(session?.type || "");
|
|
runtime.sessionName = String(session?.name || "");
|
|
runtime.status = passings.length ? "paused" : "empty";
|
|
runtime.elapsedMs = 0;
|
|
runtime.durationMs = passings.length > 1 ? Math.max(0, passings[passings.length - 1].timestamp - passings[0].timestamp) : 0;
|
|
runtime.startedWallAt = 0;
|
|
runtime.startedElapsedMs = 0;
|
|
runtime.passings = passings;
|
|
return runtime;
|
|
}
|
|
|
|
export function playSessionReplay(runtime, nowTs = Date.now()) {
|
|
if (!runtime.passings.length) return false;
|
|
if (runtime.elapsedMs >= runtime.durationMs && runtime.durationMs > 0) runtime.elapsedMs = 0;
|
|
runtime.status = "running";
|
|
runtime.startedWallAt = nowTs;
|
|
runtime.startedElapsedMs = runtime.elapsedMs;
|
|
return true;
|
|
}
|
|
|
|
export function pauseSessionReplay(runtime, nowTs = Date.now()) {
|
|
if (runtime.status !== "running") return false;
|
|
advanceSessionReplay(runtime, nowTs);
|
|
runtime.status = "paused";
|
|
return true;
|
|
}
|
|
|
|
export function resetSessionReplay(runtime) {
|
|
runtime.status = runtime.passings.length ? "paused" : "idle";
|
|
runtime.elapsedMs = 0;
|
|
runtime.startedWallAt = 0;
|
|
runtime.startedElapsedMs = 0;
|
|
return runtime;
|
|
}
|
|
|
|
export function setSessionReplaySpeed(runtime, speed, nowTs = Date.now()) {
|
|
if (runtime.status === "running") {
|
|
advanceSessionReplay(runtime, nowTs);
|
|
runtime.startedWallAt = nowTs;
|
|
runtime.startedElapsedMs = runtime.elapsedMs;
|
|
}
|
|
runtime.speed = [1, 2, 5, 10].includes(Number(speed)) ? Number(speed) : 1;
|
|
return runtime.speed;
|
|
}
|
|
|
|
export function advanceSessionReplay(runtime, nowTs = Date.now()) {
|
|
if (runtime.status !== "running") return runtime.elapsedMs;
|
|
const advanced = runtime.startedElapsedMs + Math.max(0, nowTs - runtime.startedWallAt) * runtime.speed;
|
|
runtime.elapsedMs = Math.min(runtime.durationMs, advanced);
|
|
if (runtime.elapsedMs >= runtime.durationMs) runtime.status = "finished";
|
|
return runtime.elapsedMs;
|
|
}
|
|
|
|
export function buildSessionReplaySnapshot(runtime) {
|
|
const firstTimestamp = runtime.passings[0]?.timestamp || 0;
|
|
const cutoff = firstTimestamp + Math.max(0, runtime.elapsedMs);
|
|
const visiblePassings = runtime.passings.filter((passing, index) => index === 0 || passing.timestamp <= cutoff);
|
|
const competitors = new Map();
|
|
|
|
visiblePassings.forEach((passing) => {
|
|
const key = passing.competitorKey;
|
|
const row = competitors.get(key) || {
|
|
key,
|
|
driverName: passing.driverName,
|
|
carName: passing.carName,
|
|
transponder: passing.transponder,
|
|
laps: 0,
|
|
bestLapMs: 0,
|
|
lastLapMs: 0,
|
|
firstTimestamp: passing.timestamp,
|
|
lastTimestamp: passing.timestamp,
|
|
};
|
|
row.driverName = passing.driverName || row.driverName;
|
|
row.carName = passing.carName || row.carName;
|
|
row.transponder = passing.transponder || row.transponder;
|
|
row.lastTimestamp = passing.timestamp;
|
|
if (passing.validLap !== false && passing.lapMs > 0) {
|
|
row.laps += 1;
|
|
row.lastLapMs = passing.lapMs;
|
|
row.bestLapMs = row.bestLapMs ? Math.min(row.bestLapMs, passing.lapMs) : passing.lapMs;
|
|
}
|
|
competitors.set(key, row);
|
|
});
|
|
|
|
const practiceRanking = PRACTICE_TYPES.has(runtime.sessionType);
|
|
const leaderboard = [...competitors.values()]
|
|
.map((row) => ({ ...row, totalMs: Math.max(0, row.lastTimestamp - row.firstTimestamp) }))
|
|
.sort((left, right) => {
|
|
if (practiceRanking) {
|
|
return (left.bestLapMs || Number.MAX_SAFE_INTEGER) - (right.bestLapMs || Number.MAX_SAFE_INTEGER)
|
|
|| right.laps - left.laps
|
|
|| left.driverName.localeCompare(right.driverName);
|
|
}
|
|
return right.laps - left.laps || left.totalMs - right.totalMs || left.driverName.localeCompare(right.driverName);
|
|
});
|
|
|
|
return {
|
|
status: runtime.status,
|
|
elapsedMs: runtime.elapsedMs,
|
|
durationMs: runtime.durationMs,
|
|
progress: runtime.durationMs ? Math.min(1, runtime.elapsedMs / runtime.durationMs) : visiblePassings.length ? 1 : 0,
|
|
visiblePassings,
|
|
latestPassing: visiblePassings[visiblePassings.length - 1] || null,
|
|
leaderboard,
|
|
};
|
|
}
|