stremio/apps/master-api/src/openf1/circuit.ts

131 lines
4.3 KiB
TypeScript

export type CircuitPoint = { x: number; y: number };
export type CircuitMap = {
circuitKey: number | null;
name: string | null;
rotation: number;
path: CircuitPoint[];
corners: Array<{ number: number; letter?: string; x: number; y: number }>;
};
type RawCircuit = {
circuitKey?: number;
circuitName?: string;
rotation?: number;
x?: number[];
y?: number[];
corners?: Array<{ number?: number; letter?: string; x?: number; y?: number }>;
};
function rotate(x: number, y: number, deg: number): CircuitPoint {
const rad = (deg * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return { x: x * cos + y * sin, y: -x * sin + y * cos };
}
function bounds(points: CircuitPoint[]): { minX: number; maxX: number; minY: number; maxY: number } {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (const p of points) {
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
}
return { minX, maxX, minY, maxY };
}
function normalize(p: CircuitPoint, b: { minX: number; maxX: number; minY: number; maxY: number }): CircuitPoint {
const dx = b.maxX - b.minX || 1;
const dy = b.maxY - b.minY || 1;
return {
x: (p.x - b.minX) / dx,
y: 1 - (p.y - b.minY) / dy,
};
}
export function projectCircuit(raw: RawCircuit): CircuitMap {
const rotation = Number(raw.rotation || 0);
const xs = Array.isArray(raw.x) ? raw.x : [];
const ys = Array.isArray(raw.y) ? raw.y : [];
const n = Math.min(xs.length, ys.length);
const rotated: CircuitPoint[] = [];
for (let i = 0; i < n; i++) {
const x = Number(xs[i]);
const y = Number(ys[i]);
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
rotated.push(rotate(x, y, rotation));
}
const b = rotated.length ? bounds(rotated) : { minX: 0, maxX: 1, minY: 0, maxY: 1 };
const path = rotated.map((p) => normalize(p, b));
const corners: CircuitMap["corners"] = [];
for (const c of raw.corners || []) {
const x = Number(c.x);
const y = Number(c.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
const p = normalize(rotate(x, y, rotation), b);
corners.push({ number: Number(c.number || 0), letter: c.letter, x: p.x, y: p.y });
}
return {
circuitKey: raw.circuitKey ?? null,
name: raw.circuitName ?? null,
rotation,
path,
corners,
};
}
export function projectCars(
cars: Array<{ driverNumber: number; x: number; y: number }>,
raw: RawCircuit
): Array<{ driverNumber: number; x: number; y: number }> {
const rotation = Number(raw.rotation || 0);
const xs = Array.isArray(raw.x) ? raw.x : [];
const ys = Array.isArray(raw.y) ? raw.y : [];
const n = Math.min(xs.length, ys.length);
const track: CircuitPoint[] = [];
for (let i = 0; i < n; i++) {
const x = Number(xs[i]);
const y = Number(ys[i]);
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
track.push(rotate(x, y, rotation));
}
const b = track.length ? bounds(track) : { minX: 0, maxX: 1, minY: 0, maxY: 1 };
return cars
.filter((c) => Number.isFinite(c.x) && Number.isFinite(c.y))
.map((c) => {
const p = normalize(rotate(c.x, c.y, rotation), b);
return { driverNumber: c.driverNumber, x: p.x, y: p.y };
});
}
const circuitCache = new Map<string, { at: number; map: CircuitMap; raw: RawCircuit }>();
export async function loadCircuitMap(circuitKey: number, year: number): Promise<{ map: CircuitMap; raw: RawCircuit } | null> {
const cacheKey = `${circuitKey}:${year}`;
const hit = circuitCache.get(cacheKey);
if (hit && Date.now() - hit.at < 24 * 3600_000) return hit;
const years = [year, year - 1, 2025, 2024, 2023, 2022];
for (const y of [...new Set(years)]) {
try {
const res = await fetch(`https://api.multiviewer.app/api/v1/circuits/${circuitKey}/${y}`, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(12_000),
});
if (!res.ok) continue;
const raw = (await res.json()) as RawCircuit;
const map = projectCircuit(raw);
if (map.path.length < 20) continue;
const packed = { at: Date.now(), map, raw };
circuitCache.set(cacheKey, packed);
return packed;
} catch {
/* try older year */
}
}
return null;
}