diff --git a/src/f1p/__init__.py b/src/f1p/__init__.py index e69de29..6e55e33 100644 --- a/src/f1p/__init__.py +++ b/src/f1p/__init__.py @@ -0,0 +1,107 @@ +from typing import Self + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import PStatClient, WindowProperties + +from f1p.services.data_extractor import DataExtractorService +from f1p.ui.components.leaderboard import Leaderboard +from f1p.ui.components.map import Map +from f1p.ui.components.menu import Menu +from f1p.ui.components.origin import Origin +from f1p.ui.components.playback import PlaybackControls + + +class F1PlayerApp(ShowBase): + def __init__( + self, + width: int = 800, + height: int = 800, + draw_origin: bool = False, + show_frame_rate: bool = False, + pstat_debug: bool = False, + ): + super().__init__(self) + + self.symbols_font = self.loader.loadFont("./src/f1p/ui/fonts/NotoSansSymbols2-Regular.ttf") + self.text_font = self.loader.loadFont("./src/f1p/ui/fonts/f1_font.ttf") + + self.width = width + self.height = height + + self._data_extractor: DataExtractorService | None = None + self.cam.setPos(0, -70, 40) + self.cam.lookAt(0, 0, 0) + + self.setBackgroundColor(0.3, 0.3, 0.3, 1) + + self.taskMgr.setupTaskChain("loadingData", numThreads=1) + + self.ui_components: list = [] + + if draw_origin: + origin = Origin(self.render) + origin.render() + + self.setFrameRateMeter(show_frame_rate) + + if pstat_debug: + PStatClient.connect() + + @property + def data_extractor(self) -> DataExtractorService: + if self._data_extractor is None: + self._data_extractor = DataExtractorService( + self.pixel2d, + self.taskMgr, + self.width, + self.height, + self.text_font, + ) + + return self._data_extractor + + def configure_window(self) -> Self: + props = WindowProperties() + props.setSize(self.width, self.height) + props.setFixedSize(True) + self.win.requestProperties(props) + + return self + + def draw_menu(self) -> Self: + menu = Menu(self.pixel2d, self.taskMgr, self.messenger, self.width, 40, self.text_font, self.data_extractor) + menu.render() + + return self + + def register_ui_components(self) -> Self: + playback_controls = PlaybackControls( + self.pixel2d, + self.cam, + self.taskMgr, + self.height, + self.width, + 30, + self.symbols_font, + self.text_font, + self.data_extractor, + ) + + circuit_map = Map(self.render, self.taskMgr, self.data_extractor) + + leaderboard = Leaderboard( + self.pixel2d, + self.taskMgr, + self.symbols_font, + self.text_font, + circuit_map, + self.data_extractor, + ) + + self.ui_components = [ + playback_controls, + circuit_map, + leaderboard, + ] + + return self diff --git a/src/f1p/main.py b/src/f1p/main.py index 9a220f5..221afd8 100644 --- a/src/f1p/main.py +++ b/src/f1p/main.py @@ -1,93 +1,5 @@ -from typing import Self - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import WindowProperties - -from f1p.services.data_extractor import DataExtractorService -from f1p.ui.components.leaderboard import Leaderboard -from f1p.ui.components.map import Map -from f1p.ui.components.menu import Menu -from f1p.ui.components.origin import Origin -from f1p.ui.components.playback import PlaybackControls - - -class F1PlayerApp(ShowBase): - def __init__(self, width: int = 800, height: int = 800, draw_origin: bool = False): - super().__init__(self) - - self.symbols_font = self.loader.loadFont("./src/f1p/ui/fonts/NotoSansSymbols2-Regular.ttf") - self.text_font = self.loader.loadFont("./src/f1p/ui/fonts/f1_font.ttf") - - self.width = width - self.height = height - - self._data_extractor: DataExtractorService | None = None - self.cam.setPos(0, -70, 40) - self.cam.lookAt(0, 0, 0) - - self.setBackgroundColor(0.3, 0.3, 0.3, 1) - - # self.setFrameRateMeter(True) - # PStatClient.connect() - - self.ui_components: list = [] - - if draw_origin: - origin = Origin(self.render) - origin.render() - - @property - def data_extractor(self) -> DataExtractorService: - if self._data_extractor is None: - self._data_extractor = DataExtractorService() - - return self._data_extractor - - def configure_window(self) -> Self: - props = WindowProperties() - props.setSize(self.width, self.height) - self.win.requestProperties(props) - - return self - - def draw_menu(self) -> Self: - menu = Menu(self.pixel2d, self.width, 40, self.text_font, self.data_extractor) - menu.render() - - return self - - def register_ui_components(self) -> Self: - playback_controls = PlaybackControls( - self.pixel2d, - self.cam, - self.taskMgr, - self.height, - self.width, - 30, - self.symbols_font, - self.text_font, - self.data_extractor, - ) - - circuit_map = Map(self.render, self.data_extractor) - - leaderboard = Leaderboard( - self.pixel2d, - self.symbols_font, - self.text_font, - circuit_map, - self.data_extractor, - ) - - self.ui_components = [ - playback_controls, - circuit_map, - leaderboard, - ] - - return self - +from f1p import F1PlayerApp app = F1PlayerApp() app.disableMouse() # disable camera controls -(app.configure_window().draw_menu().register_ui_components().run()) +app.configure_window().draw_menu().register_ui_components().run() diff --git a/src/f1p/services/data_extractor/__init__.py b/src/f1p/services/data_extractor/__init__.py index 3471b5a..e939a63 100644 --- a/src/f1p/services/data_extractor/__init__.py +++ b/src/f1p/services/data_extractor/__init__.py @@ -1,26 +1,45 @@ import math from pathlib import Path -from typing import Self +from typing import Any, Self import fastf1 +import numpy as np import pandas as pd +from direct.gui.DirectFrame import DirectFrame +from direct.gui.DirectWaitBar import DirectWaitBar +from direct.gui.OnscreenText import OnscreenText +from direct.showbase.DirectObject import DirectObject from direct.showbase.MessengerGlobal import messenger +from direct.task.Task import Task, TaskManager from fastf1.core import Lap, Laps, Session, Telemetry from fastf1.events import Event, EventSchedule from fastf1.mvapi import CircuitInfo -from panda3d.core import LVecBase4f, deg2Rad +from panda3d.core import LVecBase4f, NodePath, Point3, StaticTextFont, deg2Rad from pandas import DataFrame, Series, Timedelta from f1p.utils.geometry import center_pos_data, find_center, resize_pos_data -class DataExtractorService: +class DataExtractorService(DirectObject): year: int event_name: str session_id: str cache_path: Path = Path(__file__).parent.parent.parent.parent.parent / ".fastf1-cache" - def __init__(self): + def __init__( + self, + parent: NodePath, + task_manager: TaskManager, + window_width: int, + window_height: int, + text_font: StaticTextFont, + ): + self.parent = parent + self.task_manager = task_manager + self.window_width = window_width + self.window_height = window_height + self.text_font = text_font + self._event_schedule: EventSchedule | None = None self._event: Event | None = None self._session: Session | None = None @@ -39,8 +58,14 @@ def __init__(self): self.fastest_lap_telemetry: DataFrame | None = None self.map_center_coordinate: tuple[float, float, float] | None = None + self.loading_frame: DirectFrame | None = None + self.loading_text: OnscreenText | None = None + self.wait_bar: DirectWaitBar | None = None + self.processed_pos_data: DataFrame | None = None + self.accept("loadData", self.load_data) + if not self.cache_path.exists(): self.cache_path.mkdir(parents=True) @@ -278,6 +303,8 @@ def process_fastest_lap(self) -> Self: self.fastest_lap_telemetry = center_pos_data(self.map_center_coordinate, resized_pos_data_df) + self.update_loading(5) + return self def combine_position_data(self) -> Self: @@ -288,6 +315,8 @@ def combine_position_data(self) -> Self: self.processed_pos_data = pd.concat(drivers_pos_data, ignore_index=True) + self.update_loading(5) + return self def remove_records_before_session_start_time(self) -> Self: @@ -295,6 +324,8 @@ def remove_records_before_session_start_time(self) -> Self: self.processed_pos_data["SessionTime"] >= self.session_start_time ] + self.update_loading(5) + return self def normalize_position_data(self) -> Self: @@ -303,6 +334,8 @@ def normalize_position_data(self) -> Self: resized_pos_data_df = resize_pos_data(self.map_rotation, df) self.processed_pos_data = center_pos_data(self.map_center_coordinate, resized_pos_data_df) + self.update_loading(5) + return self def add_session_time_in_milliseconds(self) -> Self: @@ -310,15 +343,19 @@ def add_session_time_in_milliseconds(self) -> Self: self.processed_pos_data["SessionTimeMilliseconds"] = session_time_in_milliseconds.astype("int64") + self.update_loading(5) + return self - def add_common_session_time(self) -> Self: + def add_session_time_tick(self) -> Self: df = self.processed_pos_data.copy() df["SessionTimeTick"] = df.groupby("DriverNumber").cumcount().add(1) self.processed_pos_data = df + self.update_loading(5) + return self def process_laps(self) -> Self: @@ -352,7 +389,7 @@ def process_laps(self) -> Self: ) laps["Sector3SessionTimeMilliseconds"] = sector3_session_time_in_milliseconds.astype("int64") - lap_time_in_milliseconds = laps["LapTime"].fillna(Timedelta(milliseconds=1)).dt.total_seconds() * 1e3 + lap_time_in_milliseconds = laps["LapTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 laps["LapTimeMilliseconds"] = lap_time_in_milliseconds.astype("int64") laps["LapEndTimeMilliseconds"] = laps["LapStartTimeMilliseconds"] + laps["LapTimeMilliseconds"] @@ -383,106 +420,173 @@ def process_laps(self) -> Self: self._laps = laps + self.update_loading(5) + return self def merge_pos_and_laps(self) -> Self: - pos_data_df = self.processed_pos_data.copy() + df = self.processed_pos_data.copy() + ts_df = df[["SessionTimeTick", "SessionTimeMilliseconds"]].drop_duplicates(keep="first").copy() laps_df = self.laps.copy() - end_of_race = laps_df.loc[laps_df["LapNumber"] == self.total_laps, "LapEndTimeMilliseconds"].min() + for record in laps_df.itertuples(): + laps_df.loc[ + (laps_df["LapNumber"] == record.LapNumber) & (laps_df["DriverNumber"] == record.DriverNumber), + "SessionTimeTick", + ] = ts_df.loc[ts_df["SessionTimeMilliseconds"] <= record.LapStartTimeMilliseconds, "SessionTimeTick"].max() - for lap in laps_df.itertuples(): - pos_data_df.loc[ - (pos_data_df["DriverNumber"] == lap.DriverNumber) - & (pos_data_df["SessionTimeMilliseconds"] >= lap.LapStartTimeMilliseconds) - & (pos_data_df["SessionTimeMilliseconds"] < lap.LapEndTimeMilliseconds), - "LapNumber", - ] = lap.LapNumber + laps_df.loc[laps_df["LapNumber"] == 1.0, "SessionTimeTick"] = 1 + laps_df = laps_df.dropna(subset=["SessionTimeTick"]) + laps_df["SessionTimeTick"] = laps_df["SessionTimeTick"].astype("int64") - combined_df = pos_data_df.merge(laps_df, on=["DriverNumber", "LapNumber"], how="left").rename( - columns={"Time_x": "Time", "Time_y": "Time_Lap"}, - ) + lap_n_tick_df = laps_df[["DriverNumber", "LapNumber", "SessionTimeTick"]] + # Merge once to get he LapNumber and fill it for all SessionTimeTicks + combined_df = df.merge(lap_n_tick_df, on=["DriverNumber", "SessionTimeTick"], how="left") combined_df["LapNumber"] = combined_df.groupby("DriverNumber")["LapNumber"].ffill() - combined_df["LapStartTimeMilliseconds"] = combined_df.groupby("DriverNumber")[ - "LapStartTimeMilliseconds" - ].ffill() - combined_df["LapEndTimeMilliseconds"] = combined_df.groupby("DriverNumber")["LapEndTimeMilliseconds"].ffill() - - combined_df.loc[ - (combined_df["LapNumber"] == self.total_laps) - & (combined_df["SessionTimeMilliseconds"] > combined_df["LapEndTimeMilliseconds"]), + + # Merge second time with full laps_df to get full data per SessionTimeTick + combined_df = combined_df.merge(laps_df, on=["DriverNumber", "LapNumber"], how="left") + combined_df = combined_df.rename( + columns={ + "Time_x": "Time", + "Time_y": "TimeLap", + "SessionTimeTick_x": "SessionTimeTick", + }, + ) + combined_df = combined_df.drop(columns=["SessionTimeTick_y"]) + + self.processed_pos_data = combined_df + + self.update_loading(5) + + return self + + def compute_lap_completion(self) -> Self: + df = self.processed_pos_data.copy() + + df["LapStartTimeMilliseconds"] = df.groupby("DriverNumber")["LapStartTimeMilliseconds"].ffill() + df["LapEndTimeMilliseconds"] = df.groupby("DriverNumber")["LapEndTimeMilliseconds"].ffill() + + df.loc[ + (df["LapNumber"] == self.total_laps) & (df["SessionTimeMilliseconds"] > df["LapEndTimeMilliseconds"]), "LapNumber", ] = self.total_laps + 1 - combined_df["ElapsedTimeSinceStartOfLapMilliseconds"] = ( - combined_df["SessionTimeMilliseconds"] - combined_df["LapStartTimeMilliseconds"] - ) - combined_df["LapPercentageCompletion"] = ( - combined_df["ElapsedTimeSinceStartOfLapMilliseconds"] / combined_df["LapTimeMilliseconds"] - ) - combined_df["LapPercentageCompletion"] = combined_df["LapPercentageCompletion"].fillna(0) - combined_df["LapsCompletion"] = (combined_df["LapNumber"] - 1) + combined_df["LapPercentageCompletion"] - combined_df["PositionIndex"] = ( - combined_df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]) + df["ElapsedTimeSinceStartOfLapMilliseconds"] = df["SessionTimeMilliseconds"] - df["LapStartTimeMilliseconds"] + df["LapPercentageCompletion"] = df["ElapsedTimeSinceStartOfLapMilliseconds"] / df["LapTimeMilliseconds"] + df["LapPercentageCompletion"] = df["LapPercentageCompletion"].replace([np.inf, -np.inf], 0) + df.loc[df["LapNumber"] > self.total_laps, "LapPercentageCompletion"] = 0 + df["LapsCompletion"] = (df["LapNumber"] - 1) + df["LapPercentageCompletion"] + + self.processed_pos_data = df + + self.update_loading(5) + + return self + + def compute_is_dnf(self) -> Self: + df = self.processed_pos_data.copy() + + df.loc[df["Position"].notna(), "IsDNF"] = False + df.loc[df["Position"].isna(), "IsDNF"] = True + + self.processed_pos_data = df + + self.update_loading(5) + + return self + + def compute_is_finished(self) -> Self: + df = self.processed_pos_data.copy() + df.loc[df["LapsCompletion"] == self.total_laps, "IsFinished"] = True + df.loc[df["IsFinished"].isna(), "IsFinished"] = False + df.loc[df["IsFinished"], "IsDNF"] = False + + self.processed_pos_data = df + + self.update_loading(5) + + return self + + def compute_position_index(self) -> Self: + df = self.processed_pos_data.copy() + laps_df = self.laps.copy() + end_of_race = laps_df.loc[laps_df["LapNumber"] == self.total_laps, "LapEndTimeMilliseconds"].min() + + df["PositionIndex"] = ( + df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]) .groupby("SessionTimeTick") .cumcount() .add(1) - 1 ) + df.loc[df["SessionTimeMilliseconds"] >= end_of_race, "PositionIndex"] = pd.NA + df["PositionIndex"] = df.groupby("DriverNumber")["PositionIndex"].ffill().astype("int64") - combined_df.loc[combined_df["Position"].notna(), "IsDNF"] = False - combined_df.loc[combined_df["Position"].isna(), "IsDNF"] = True + self.processed_pos_data = df - combined_df.loc[combined_df["LapsCompletion"] == self.total_laps, "IsFinished"] = True - combined_df.loc[combined_df["IsFinished"].isna(), "IsFinished"] = False - combined_df.loc[combined_df["IsFinished"], "IsDNF"] = False + self.update_loading(5) + + return self - combined_df.loc[combined_df["SessionTimeMilliseconds"] >= end_of_race, "PositionIndex"] = pd.NA - combined_df["PositionIndex"] = combined_df.groupby("DriverNumber")["PositionIndex"].ffill().astype("int64") + def compute_fastest_lap(self) -> Self: + df = self.processed_pos_data.copy() - combined_df["FastestLapTimeMilliseconds"] = combined_df.sort_values( + df["FastestLapTimeMilliseconds"] = df.sort_values( by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False], )["FastestLapTimeMillisecondsSoFar"].cummin() - combined_df.loc[ - combined_df["FastestLapTimeMillisecondsSoFar"] == combined_df["FastestLapTimeMilliseconds"], - "HasFastestLap", - ] = True - combined_df.loc[combined_df["HasFastestLap"].isna(), "HasFastestLap"] = False - combined_df["HasFastestLap"] = combined_df.groupby("DriverNumber")["HasFastestLap"].ffill() + df.loc[df["FastestLapTimeMillisecondsSoFar"] == df["FastestLapTimeMilliseconds"], "HasFastestLap"] = True + df.loc[df["HasFastestLap"].isna(), "HasFastestLap"] = False + df["HasFastestLap"] = df.groupby("DriverNumber")["HasFastestLap"].ffill() - combined_df.loc[ - (combined_df["SessionTimeMilliseconds"] >= combined_df["Sector1SessionTimeMilliseconds"]), - "DiffToCarInFront", - ] = combined_df["S1DiffToCarAhead"] - combined_df.loc[ - (combined_df["SessionTimeMilliseconds"] >= combined_df["Sector2SessionTimeMilliseconds"]), + self.processed_pos_data = df + + self.update_loading(5) + + return self + + def compute_diff_to_car_in_front(self) -> Self: + df = self.processed_pos_data.copy() + df.loc[ + (df["SessionTimeMilliseconds"] >= df["Sector1SessionTimeMilliseconds"]), "DiffToCarInFront", - ] = combined_df["S2DiffToCarAhead"] - combined_df.loc[ - (combined_df["SessionTimeMilliseconds"] >= combined_df["Sector3SessionTimeMilliseconds"]), + ] = df["S1DiffToCarAhead"] + df.loc[ + (df["SessionTimeMilliseconds"] >= df["Sector2SessionTimeMilliseconds"]), "DiffToCarInFront", - ] = combined_df["S3DiffToCarAhead"] - combined_df.loc[ - (combined_df["PositionIndex"] == 0), + ] = df["S2DiffToCarAhead"] + df.loc[ + (df["SessionTimeMilliseconds"] >= df["Sector3SessionTimeMilliseconds"]), "DiffToCarInFront", - ] = 0 - combined_df["DiffToCarInFront"] = combined_df.groupby("DriverNumber")["DiffToCarInFront"].ffill() - combined_df["DiffToCarInFront"] = round(combined_df["DiffToCarInFront"] / 1000, 3) + ] = df["S3DiffToCarAhead"] + df.loc[df["PositionIndex"] == 0, "DiffToCarInFront"] = 0 + df["DiffToCarInFront"] = df.groupby("DriverNumber")["DiffToCarInFront"].ffill() + df["DiffToCarInFront"] = round(df["DiffToCarInFront"] / 1000, 3) + + self.processed_pos_data = df + + self.update_loading(5) + + return self - combined_df["DiffToLeader"] = ( - combined_df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]) + def compute_diff_to_leader(self) -> Self: + df = self.processed_pos_data.copy() + df["DiffToLeader"] = ( + df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]) .groupby(["SessionTimeTick"])["DiffToCarInFront"] .cumsum() ) - combined_df["DiffToLeader"] = round(combined_df["DiffToLeader"], 3) + df["DiffToLeader"] = round(df["DiffToLeader"], 3) - self.processed_pos_data = combined_df + self.processed_pos_data = df + + self.update_loading(5) return self - def add_in_pit_column(self) -> Self: + def compute_in_pit(self) -> Self: df = self.processed_pos_data.copy() df.loc[ @@ -500,9 +604,11 @@ def add_in_pit_column(self) -> Self: self.processed_pos_data = df + self.update_loading(5) + return self - def process_tire_compound_columns(self) -> Self: + def compute_tire_compound(self) -> Self: df = self.processed_pos_data.copy() df["Compound"] = df["Compound"].str[0].astype("string") @@ -523,10 +629,54 @@ def process_tire_compound_columns(self) -> Self: columns=["SCompoundColor", "MCompoundColor", "HCompoundColor", "ICompoundColor", "WCompoundColor"], ) + self.update_loading(5) + return self - def extract(self): + def render_wait_bar(self) -> None: + width = 400 + height = 200 + self.loading_frame = DirectFrame( + parent=self.parent, + frameColor=(0.20, 0.20, 0.20, 0.7), + frameSize=(0, width, 0, -height), + pos=Point3((self.window_width / 2) - (width / 2), 0, -((self.window_height / 2) - (height / 2))), + ) + + self.loading_text = OnscreenText( + parent=self.loading_frame, + pos=(width / 2, -(height / 2)), + scale=width / 10, + fg=(1, 1, 1, 0.8), + font=self.text_font, + text="Loading ...", + ) + + self.wait_bar = DirectWaitBar( + parent=self.loading_frame, + text="WaitBar", + value=0, + range=100, + barColor=(0, 1, 0, 0.7), + frameSize=(0, width - 20, 0, -10), + pos=Point3(10, 0, -(height - 20)), + ) + + def update_loading(self, value: int) -> None: + self.wait_bar["value"] += value + + def delete_loading(self) -> None: + self.wait_bar.destroy() + self.loading_text.destroy() + self.loading_frame.destroy() + + def load_data(self) -> None: + self.render_wait_bar() + self.task_manager.add(self.extract, "extractData", taskChain="loadingData") + + def extract(self, task: Task) -> Any: self.session.load() + self.update_loading(10) ( self.process_fastest_lap() @@ -534,11 +684,21 @@ def extract(self): .remove_records_before_session_start_time() .normalize_position_data() .add_session_time_in_milliseconds() - .add_common_session_time() + .add_session_time_tick() .process_laps() .merge_pos_and_laps() - .add_in_pit_column() - .process_tire_compound_columns() + .compute_lap_completion() + .compute_is_dnf() + .compute_is_finished() + .compute_position_index() + .compute_fastest_lap() + .compute_diff_to_car_in_front() + .compute_diff_to_leader() + .compute_in_pit() + .compute_tire_compound() ) + self.delete_loading() messenger.send("sessionSelected") + + return task.done diff --git a/src/f1p/ui/components/leaderboard/__init__.py b/src/f1p/ui/components/leaderboard/__init__.py index 3079d00..e3de613 100644 --- a/src/f1p/ui/components/leaderboard/__init__.py +++ b/src/f1p/ui/components/leaderboard/__init__.py @@ -1,7 +1,10 @@ +from typing import Any + from direct.gui.DirectFrame import DirectFrame from direct.gui.OnscreenImage import OnscreenImage from direct.gui.OnscreenText import OnscreenText from direct.showbase.DirectObject import DirectObject +from direct.task.Task import Task, TaskManager from fastf1.core import Laps from panda3d.core import Point3, StaticTextFont, TextNode, TransparencyAttrib @@ -21,6 +24,7 @@ class Leaderboard(DirectObject): def __init__( self, pixel2d, + task_manager: TaskManager, symbols_font: StaticTextFont, text_font: StaticTextFont, circuit_map: Map, @@ -29,6 +33,7 @@ def __init__( super().__init__() self.pixel2d = pixel2d + self.task_manager = task_manager self.width = 215 self._height: float | None = None self.symbols_font = symbols_font @@ -36,7 +41,7 @@ def __init__( self.circuit_map = circuit_map self.data_extractor = data_extractor - self.accept("sessionSelected", self.render) + self.accept("sessionSelected", self.render_task) self.accept("updateLeaderboard", self.update) self.frame: DirectFrame | None = None @@ -307,7 +312,10 @@ def update(self, session_time_tick: int) -> None: processor.update(session_time_tick) - def render(self) -> None: + def render_task(self) -> None: + self.task_manager.add(self.render, "renderLeaderboard") + + def render(self, task: Task) -> Any: self.render_frame() self.render_f1_logo() self.render_lap_counter() @@ -315,3 +323,5 @@ def render(self) -> None: self.render_track_status() self.render_mode_selector() self.render_drivers() + + return task.done diff --git a/src/f1p/ui/components/leaderboard/processors/__init__.py b/src/f1p/ui/components/leaderboard/processors/__init__.py index 3c1bb5f..8ab2b7d 100644 --- a/src/f1p/ui/components/leaderboard/processors/__init__.py +++ b/src/f1p/ui/components/leaderboard/processors/__init__.py @@ -157,8 +157,8 @@ def update_times(self, driver: Driver, current_record: Series, index: int) -> No if current_color != default_color: self.driver_times[index]["fg"] = default_color - if self.driver_times[index]["text"] != f"+{current_record['DiffToCarInFront']}": - self.driver_times[index]["text"] = f"+{current_record['DiffToCarInFront']}" + if self.driver_times[index]["text"] != f"+{current_record['DiffToCarInFront']:.3f}": + self.driver_times[index]["text"] = f"+{current_record['DiffToCarInFront']:.3f}" def update_tire_compound(self, driver: Driver, current_record: Series, index: int) -> None: if driver.is_dnf: @@ -217,8 +217,8 @@ def update_times(self, driver: Driver, current_record: Series, index: int) -> No if current_color != default_color: self.driver_times[index]["fg"] = default_color - if self.driver_times[index]["text"] != f"+{current_record['DiffToLeader']}": - self.driver_times[index]["text"] = f"+{current_record['DiffToLeader']}" + if self.driver_times[index]["text"] != f"+{current_record['DiffToLeader']:.3f}": + self.driver_times[index]["text"] = f"+{current_record['DiffToLeader']:.3f}" class TiresLeaderboardProcessor(IntervalLeaderboardProcessor): diff --git a/src/f1p/ui/components/map.py b/src/f1p/ui/components/map.py index 61a44b7..fdc3836 100644 --- a/src/f1p/ui/components/map.py +++ b/src/f1p/ui/components/map.py @@ -1,5 +1,8 @@ +from typing import Any + import numpy as np from direct.showbase.DirectObject import DirectObject +from direct.task.Task import Task, TaskManager from panda3d.core import LineSegs, NodePath from pandas import DataFrame @@ -8,10 +11,11 @@ class Map(DirectObject): - def __init__(self, parent: NodePath, data_extractor: DataExtractorService): + def __init__(self, parent: NodePath, task_manager: TaskManager, data_extractor: DataExtractorService): super().__init__() self.parent = parent + self.task_manager = task_manager self.data_extractor = data_extractor self.inner_border_node_path: NodePath | None = None @@ -21,8 +25,8 @@ def __init__(self, parent: NodePath, data_extractor: DataExtractorService): self._pos_data: DataFrame | None = None self._map_center_coordinate: list[float] | None = None - self.accept("sessionSelected", self.select_session) - self.accept("clearMaps", self.clear_out_maps) + self.accept("sessionSelected", self.render_task) + # self.accept("clearMaps", self.clear_out_maps) def render_map(self, df: DataFrame) -> None: new_df = df.copy() @@ -102,6 +106,11 @@ def initialize_drivers(self) -> None: self.drivers.append(driver) - def select_session(self) -> None: + def render_task(self) -> None: + self.task_manager.add(self.render, "renderMap") + + def render(self, task: Task) -> Any: self.render_map(self.data_extractor.fastest_lap_telemetry) self.initialize_drivers() + + return task.done diff --git a/src/f1p/ui/components/menu.py b/src/f1p/ui/components/menu.py index 8b31a16..b41627c 100644 --- a/src/f1p/ui/components/menu.py +++ b/src/f1p/ui/components/menu.py @@ -2,7 +2,9 @@ from direct.gui.DirectFrame import DirectFrame from direct.gui.DirectOptionMenu import DirectOptionMenu +from direct.showbase.Messenger import Messenger from direct.showbase.MessengerGlobal import messenger +from direct.task.Task import TaskManager from panda3d.core import Point3, StaticTextFont from f1p.services.data_extractor import DataExtractorService @@ -14,12 +16,16 @@ class Menu: def __init__( self, pixel2d, + task_manager: TaskManager, + messenger: Messenger, width: int, height: int, text_font: StaticTextFont, data_extractor: DataExtractorService, ): self.pixel2d = pixel2d + self.task_manager = task_manager + self.messenger = messenger self.width = width self.height = height self.text_font = text_font @@ -111,9 +117,12 @@ def select_session(self, session_id: str) -> None: self.data_extractor._session = None self.data_extractor._fastest_lap = None self.data_extractor._circuit_info = None - messenger.send("clearMaps") + self.messenger.send("clearMaps") self.data_extractor.session_id = session_id - self.data_extractor.extract() + + self.messenger.send("loadData") + + # self.data_extractor.extract() def render_session_menu(self) -> None: self.session_menu = BlackDropDown( diff --git a/src/f1p/ui/components/playback.py b/src/f1p/ui/components/playback.py index a990d0b..9ba34fe 100644 --- a/src/f1p/ui/components/playback.py +++ b/src/f1p/ui/components/playback.py @@ -1,4 +1,5 @@ from math import cos, sin +from typing import Any from direct.gui.DirectButton import DirectButton from direct.gui.DirectFrame import DirectFrame @@ -6,7 +7,7 @@ from direct.gui.DirectSlider import DirectSlider from direct.showbase.DirectObject import DirectObject from direct.showbase.MessengerGlobal import messenger -from direct.task.Task import TaskManager +from direct.task.Task import Task, TaskManager from panda3d.core import Camera, Point3, StaticTextFont, TextNode, deg2Rad from f1p.services.data_extractor import DataExtractorService @@ -39,7 +40,7 @@ def __init__( self.text_font = text_font self.data_extractor = data_extractor - self.accept("sessionSelected", self.render) + self.accept("sessionSelected", self.render_task) self.frame: DirectFrame | None = None self.play_button: DirectButton | None = None @@ -214,7 +215,10 @@ def render_camera_button(self) -> None: pos=Point3(self.width - 40, 0, -self.height / 2), ) - def render(self): + def render_task(self) -> None: + self.task_manager.add(self.render, "renderPlayback") + + def render(self, task: Task) -> Any: self.task_manager.add(self.move_camera, "move_camera") self.task_manager.add(self.move_timeline, "move_timeline") self.render_frame() @@ -222,3 +226,5 @@ def render(self): self.render_timeline() self.render_playback_speed_button() self.render_camera_button() + + return task.done