| """
|
| replay_data.py
|
| --------------
|
| Loads lap-by-lap car positions from FastF1 for a given session.
|
| Used to animate the track map in the replay tab.
|
| """
|
|
|
| import fastf1
|
| import pandas as pd
|
| import numpy as np
|
|
|
|
|
| def load_lap_positions(year: int, round_num: int, session_type: str = "R") -> dict:
|
| """
|
| Load lap-by-lap car positions using the car's actual position
|
| at the END of each lap (not midpoint), matched to the track reference line.
|
| """
|
| fastf1.Cache.enable_cache("f1_cache")
|
|
|
| session = fastf1.get_session(year, round_num, session_type)
|
| session.load(telemetry=True, weather=False, messages=False)
|
|
|
| laps = session.laps
|
| max_lap = int(laps["LapNumber"].max())
|
|
|
|
|
| fastest = session.laps.pick_fastest()
|
| ref_tel = fastest.get_telemetry()[["X", "Y"]].dropna()
|
| ref_points = ref_tel.values
|
|
|
| lap_data = {}
|
|
|
| for lap_num in range(1, max_lap + 1):
|
| lap_data[lap_num] = {}
|
| lap_slice = laps[laps["LapNumber"] == lap_num]
|
|
|
| for _, lap_row in lap_slice.iterrows():
|
| driver = lap_row["Driver"]
|
| try:
|
| tel = lap_row.get_telemetry()[["X", "Y", "Distance"]].dropna()
|
| if tel is None or tel.empty:
|
| continue
|
|
|
|
|
|
|
| mid_dist = tel["Distance"].max() * 0.5
|
| closest_idx = (tel["Distance"] - mid_dist).abs().argmin()
|
| x = float(tel["X"].iloc[closest_idx])
|
| y = float(tel["Y"].iloc[closest_idx])
|
|
|
| pos = lap_row.get("Position", None)
|
| compound = lap_row.get("Compound", "UNKNOWN")
|
|
|
| lap_data[lap_num][driver] = {
|
| "x": x,
|
| "y": y,
|
| "position": int(pos) if pd.notna(pos) else 99,
|
| "compound": str(compound) if pd.notna(compound) else "UNKNOWN",
|
| }
|
| except Exception as e:
|
| continue
|
|
|
| return lap_data
|
| """
|
| Load telemetry and return car (x, y) positions for each lap.
|
|
|
| Returns:
|
| {
|
| lap_number: {
|
| 'VER': {'x': 1234, 'y': 5678, 'position': 1, 'compound': 'SOFT'},
|
| 'NOR': {...},
|
| ...
|
| }
|
| }
|
| """
|
| fastf1.Cache.enable_cache("f1_cache")
|
|
|
| session = fastf1.get_session(year, round_num, session_type)
|
| session.load(telemetry=True, weather=False, messages=False)
|
|
|
| laps = session.laps
|
| max_lap = int(laps["LapNumber"].max())
|
|
|
| lap_data = {}
|
|
|
| for lap_num in range(1, max_lap + 1):
|
| lap_data[lap_num] = {}
|
| lap_slice = laps[laps["LapNumber"] == lap_num]
|
|
|
| for _, lap_row in lap_slice.iterrows():
|
| driver = lap_row["Driver"]
|
| try:
|
| tel = lap_row.get_telemetry()
|
| if tel is None or tel.empty:
|
| continue
|
|
|
|
|
| mid = len(tel) // 2
|
| x = float(tel["X"].iloc[mid])
|
| y = float(tel["Y"].iloc[mid])
|
|
|
|
|
| pos = lap_row.get("Position", None)
|
| compound = lap_row.get("Compound", "UNKNOWN")
|
|
|
| lap_data[lap_num][driver] = {
|
| "x": x,
|
| "y": y,
|
| "position": int(pos) if pd.notna(pos) else 99,
|
| "compound": compound if pd.notna(compound) else "UNKNOWN",
|
| }
|
| except Exception:
|
| continue
|
|
|
| return lap_data
|
|
|
|
|
| def get_track_outline(year: int, round_num: int, session_type: str = "R"):
|
| """
|
| Returns (x_coords, y_coords) arrays tracing the full circuit outline.
|
| Uses the fastest lap's telemetry as the reference line.
|
| """
|
| fastf1.Cache.enable_cache("f1_cache")
|
|
|
| session = fastf1.get_session(year, round_num, session_type)
|
| session.load(telemetry=True, weather=False, messages=False)
|
|
|
| fastest = session.laps.pick_fastest()
|
| tel = fastest.get_telemetry()
|
|
|
| return tel["X"].values, tel["Y"].values
|
| def get_driver_color(driver: str) -> tuple:
|
| """
|
| Returns an RGB tuple for each driver.
|
| Colors roughly match 2024 F1 team colors.
|
| """
|
| COLORS = {
|
| "VER": (30, 65, 255),
|
| "PER": (30, 65, 255),
|
| "HAM": (0, 210, 190),
|
| "RUS": (0, 210, 190),
|
| "LEC": (220, 0, 0),
|
| "SAI": (220, 0, 0),
|
| "NOR": (255, 135, 0),
|
| "PIA": (255, 135, 0),
|
| "ALO": (0, 110, 120),
|
| "STR": (0, 110, 120),
|
| "GAS": (0, 160, 222),
|
| "OCO": (0, 160, 222),
|
| "ALB": (0, 130, 250),
|
| "SAR": (0, 130, 250),
|
| "TSU": (99, 0, 114),
|
| "LAW": (99, 0, 114),
|
| "HUL": (144, 0, 0),
|
| "MAG": (144, 0, 0),
|
| "BOT": (130, 0, 200),
|
| "ZHO": (130, 0, 200),
|
| }
|
| return COLORS.get(driver, (180, 180, 180))
|
|
|
|
|
| def precompute_frames(lap_data: dict) -> list:
|
| """
|
| Flatten lap_data into a list of frames for smooth animation.
|
| Each frame = one lap snapshot.
|
|
|
| Returns:
|
| List of dicts: [{lap: int, drivers: {driver: {x,y,position,compound}}}, ...]
|
| """
|
| frames = []
|
| for lap_num in sorted(lap_data.keys()):
|
| frames.append({
|
| "lap": lap_num,
|
| "drivers": lap_data[lap_num],
|
| })
|
| return frames |