131 lines
4.3 KiB
TypeScript
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;
|
|
}
|