// components/live/LiveTrackMap.tsx 'use client'; import { useEffect, useState } from 'react'; import { getTrackMapUrl, cleanTrackName, cleanTrackConfig } from '@/lib/trackUtils'; import { getTrackMapConfig, worldToMapCoords, type TrackMapConfig } from '@/lib/trackMapConfig'; interface Car { carID: number; driver_name: string; car_model: string; normalizedSplinePos: number; position: number; lap_time?: number; best_lap_time?: number; world_position?: { x: number; y: number; z: number }; } interface LiveTrackMapProps { track: string; trackConfig: string; cars: Car[]; } export default function LiveTrackMap({ track, trackConfig, cars }: LiveTrackMapProps) { const [imageError, setImageError] = useState(false); const [mapConfig, setMapConfig] = useState(null); // Static track bounds - set once and never change const [trackBounds, setTrackBounds] = useState<{ minX: number; maxX: number; minZ: number; maxZ: number; } | null>(null); // Add mouse position state const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null); // Define event handlers const handleMouseMove = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * 100; const y = ((e.clientY - rect.top) / rect.height) * 100; setMousePos({ x, y }); }; const handleMouseLeave = () => { setMousePos(null); }; const trackMapUrl = getTrackMapUrl(track, trackConfig); const displayTrackName = cleanTrackName(track); const displayTrackConfig = cleanTrackConfig(trackConfig); useEffect(() => { getTrackMapConfig(track, trackConfig).then(config => { setMapConfig(config); if (config) { const halfWidth = config.width / 2; const halfHeight = config.height / 2; setTrackBounds({ minX: config.xOffset - halfWidth, maxX: config.xOffset + halfWidth, minZ: config.zOffset - halfHeight, maxZ: config.zOffset + halfHeight, }); } }); }, [track, trackConfig]); // Convert world position to screen position const getCarPosition = (car: Car) => { // Use AC's formula with map.ini config if (car.world_position && car.world_position.x !== undefined && mapConfig) { const pos = worldToMapCoords( car.world_position.x, car.world_position.y, car.world_position.z, mapConfig ); // Debug first car only if (cars.indexOf(car) === 0) { console.log(`[Map] World: (${car.world_position.x.toFixed(1)}, ${car.world_position.z.toFixed(1)}) -> Screen: (${pos.x.toFixed(1)}%, ${pos.y.toFixed(1)}%)`); } return pos; } // Fallback: distribute along diagonal based on normalizedSplinePos const fallbackX = car.normalizedSplinePos * 100; const fallbackY = car.normalizedSplinePos * 100; return { x: fallbackX, y: fallbackY }; }; // Get color based on position const getPositionColor = (position: number) => { if (!position || position === 0) return '#6b7280'; // Gray for no position if (position === 1) return '#ffffff'; // P1 - White if (position === 2) return '#d1d5db'; // P2 - Light gray if (position === 3) return '#9ca3af'; // P3 - Gray return '#6b7280'; // Others - Dark gray }; return (
{/* Track Map Background */} {!imageError ? ( {`${track} setImageError(true)} /> ) : (
Track map not available
)} {/* Grid overlay for reference */}
{/* Car Positions */}
{cars.map((car) => { const pos = getCarPosition(car); const color = getPositionColor(car.position); return (
{/* Car dot */}
{/* Position number */} {car.position > 0 && (
P{car.position}
)} {/* Driver name on hover */}
{car.driver_name}
); })}
{/* Track info overlay */}
TRACK
{track}
{trackConfig && trackConfig !== 'default' && (
{trackConfig}
)}
{/* Live indicator */}
LIVE
); }