import { useMemo, useState } from 'react' import { Loader2, MapPin, Satellite } from 'lucide-react' import type { Driver, EnrichedResult, ReplayFramesResponse, TrackOutline } from '../types' import { buildOutlinePath } from '../lib/trackmap' import { interpolateReplayCars, replayCarToSvg } from '../lib/replay' import { EmptyStateCard } from './EmptyStateCard' import '../styles/replay-map.css' interface Props { outline?: TrackOutline | null replay?: ReplayFramesResponse | null tMs: number drivers: Driver[] results: EnrichedResult[] loading?: boolean error?: boolean } export function ReplayTrackMap({ outline, replay, tMs, drivers, results, loading = false, error = false, }: Props) { const [pinned, setPinned] = useState(null) const outlinePath = useMemo(() => buildOutlinePath(outline?.points ?? []), [outline]) const driverInfo = useMemo(() => { const info = new Map() for (const driver of drivers) { info.set(String(driver.driver_number), { label: driver.name_acronym || String(driver.driver_number), color: normalizeColor(driver.team_colour), }) } for (const result of results) { const key = String(result.driver_number) if (!info.has(key)) { info.set(key, { label: result.name_acronym || key, color: normalizeColor(result.team_colour), }) } } return info }, [drivers, results]) const cars = useMemo(() => { if (!outline?.bounds || !replay?.frames?.length) return [] const positions = interpolateReplayCars(replay.frames, tMs) return Object.entries(positions) .map(([number, car]) => ({ number, svg: replayCarToSvg(car, outline.bounds), info: driverInfo.get(number), })) .sort((a, b) => Number(a.number) - Number(b.number)) }, [driverInfo, outline?.bounds, replay?.frames, tMs]) if (loading) { return (
) } if (error) { return (
) } if (!outline || !outlinePath) { return (
) } if (!replay?.frames?.length || cars.length === 0) { return (
) } return (
{cars.map((car) => { const label = car.info?.label ?? car.number const selected = pinned === car.number return ( setPinned(selected ? null : car.number)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() setPinned(selected ? null : car.number) } }} > {label} {label} ) })}
) } function normalizeColor(color: string | undefined): string { const raw = color?.trim() if (!raw) return '#777777' return raw.startsWith('#') ? raw : `#${raw}` }