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(); 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; }