fastlap_app.py
  1  import dash
  2  import dash_bootstrap_components as dbc
  3  import pandas as pd
  4  from dash import dash_table, dcc, html
  5  from dash.dependencies import Input, Output
  6  
  7  # from django.db.models import Count
  8  from django_plotly_dash import DjangoDash
  9  
 10  from telemetry.models import Driver
 11  from telemetry.pitcrew.history import History
 12  from telemetry.pitcrew.message import MessageTrackGuide
 13  from telemetry.racing_stats import RacingStats
 14  from telemetry.visualizer import fig_add_features, lap_fig
 15  
 16  app = DjangoDash("Fastlap", serve_locally=True, add_bootstrap_links=True)
 17  
 18  app.layout = html.Div(
 19      [
 20          dcc.Dropdown(id="game-dropdown", multi=False, placeholder="Select Game"),
 21          dcc.Dropdown(id="car-dropdown", multi=False, placeholder="Select Car"),
 22          dcc.Dropdown(id="track-dropdown", multi=False, placeholder="Select Track"),
 23          html.Div(id="data-table"),
 24          dbc.Alert("fast lap", id="fast-lap-info", color="info"),
 25          html.Div(id="graphs-container"),
 26          html.Button("Submit", id="submit-val", n_clicks=0, style={"display": "none"}),
 27      ]
 28  )
 29  
 30  
 31  @app.callback(Output("game-dropdown", "options"), Output("game-dropdown", "value"), [Input("submit-val", "n_clicks")])
 32  def set_games_options(n_clicks, session_state=None):
 33      racing_stats = RacingStats()
 34      laps = list(racing_stats.fast_lap_values())
 35      games = sorted(set(lap["game__name"] for lap in laps))
 36      game = None
 37      if session_state is not None:
 38          game = session_state.get("game__name")
 39      return [{"label": game, "value": game} for game in games], game
 40  
 41  
 42  @app.callback(
 43      Output("car-dropdown", "options"),
 44      Output("car-dropdown", "value"),
 45      Output("car-dropdown", "disabled"),
 46      [Input("game-dropdown", "value")],
 47  )
 48  def set_cars_options(game, session_state=None):
 49      if game is not None:
 50          racing_stats = RacingStats()
 51          laps = list(racing_stats.fast_lap_values())
 52          cars = sorted(set(lap["car__name"] for lap in laps if lap["game__name"] == game))
 53          car = None
 54          if session_state is not None:
 55              # set game to session_state
 56              session_state["game__name"] = game
 57              car = session_state.get("car__name")
 58          return [{"label": car, "value": car} for car in cars], car, False
 59      return [], None, True
 60  
 61  
 62  @app.callback(
 63      Output("track-dropdown", "options"),
 64      Output("track-dropdown", "value"),
 65      Output("track-dropdown", "disabled"),
 66      [Input("game-dropdown", "value"), Input("car-dropdown", "value")],
 67  )
 68  def set_tracks_options(game, car, session_state=None):
 69      if game is not None and car is not None:
 70          racing_stats = RacingStats()
 71          laps = list(racing_stats.fast_lap_values())
 72          tracks = sorted(
 73              set(lap["track__name"] for lap in laps if lap["game__name"] == game and lap["car__name"] == car)
 74          )
 75          track = None
 76          if session_state is not None:
 77              # set game and car to session_state
 78              session_state["game__name"] = game
 79              session_state["car__name"] = car
 80              track = session_state.get("track__name")
 81          return [{"label": track, "value": track} for track in tracks], track, False
 82      return [], None, True
 83  
 84  
 85  @app.callback(
 86      Output("data-table", "children"),
 87      [Input("game-dropdown", "value"), Input("car-dropdown", "value"), Input("track-dropdown", "value")],
 88  )
 89  def update_table(game, car, track, session_state=None):
 90      # if all([game, car, track]):
 91      #     return dash.no_update
 92  
 93      if session_state is not None:
 94          session_state["game__name"] = game
 95          session_state["car__name"] = car
 96          session_state["track__name"] = track
 97  
 98      racing_stats = RacingStats()
 99      # laps = racing_stats.fast_lap_values()
100      laps = racing_stats.known_combos(valid=True)
101      # laps = laps.annotate(count=Count("id"))
102      laps = list(laps)
103  
104      data = [
105          lap
106          for lap in laps
107          if (game is None or lap["track__game__name"] == game)
108          and (car is None or lap["car__name"] == car)
109          and (track is None or lap["track__name"] == track)
110      ]
111  
112      return dash_table.DataTable(
113          columns=[
114              {"name": "Game", "id": "track__game__name"},
115              {"name": "Car", "id": "car__name"},
116              {"name": "Track", "id": "track__name"},
117              {"name": "Laps", "id": "count"},
118          ],
119          data=data,
120          style_cell={"textAlign": "left"},
121          style_as_list_view=True,
122          cell_selectable=False,
123          # row_selectable=True,
124          style_header={"backgroundColor": "lightgrey", "fontWeight": "bold"},
125      )
126  
127  
128  # https://dash.plotly.com/datatable
129  # @app.callback(Output('tbl_out', 'children'), Input('tbl', 'active_cell'))
130  # def update_graphs(active_cell):
131  #     return str(active_cell) if active_cell else "Click the table"
132  
133  
134  @app.callback(
135      Output("graphs-container", "children"),
136      Output("fast-lap-info", "children"),
137      [
138          Input("submit-val", "n_clicks"),
139          Input("game-dropdown", "value"),
140          Input("car-dropdown", "value"),
141          Input("track-dropdown", "value"),
142      ],
143  )
144  def update_graph(n_clicks, game, car, track, session_state=None):
145      if n_clicks is None or not all([game, car, track]):
146          return dash.no_update
147  
148      driver = None
149      if session_state is not None:
150          session_state["game__name"] = game
151          session_state["car__name"] = car
152          session_state["track__name"] = track
153  
154          driver_name = session_state.get("driver_name")
155          if driver_name is not None:
156              driver = Driver.objects.filter(name=driver_name).first()
157  
158      racing_stats = RacingStats()
159      fast_laps = list(racing_stats.fast_laps(game=game, track=track, car=car))
160      laps_count = racing_stats.laps(game=game, track=track, car=car, valid=True).count()
161  
162      if len(fast_laps) == 0:
163          return dash.no_update
164      if driver is not None:
165          driver_name = driver.name
166      else:
167          driver_name = "Jim"
168  
169      history = History()
170      history.set_filter({"GameName": game, "TrackCode": track, "CarModel": car, "Driver": driver_name})
171      history.init()
172      # fast_lap = fast_laps[0]
173      # segments = fast_lap.data.get("segments", [])
174      # for i, segment in enumerate(segments):
175      #     next_index = (i + 1) % len(segments)
176      #     previous_index = (i - 1) % len(segments)
177      #     segment.previous_segment = segments[previous_index]
178      #     segment.next_segment = segments[next_index]
179  
180      segments = history.segments
181  
182      graphs = []
183      if driver is not None:
184          driver_name = driver.name
185          graphs.append(html.H2(f"Showing stats for driver {driver_name}"))
186          delta = history.driver_delta()
187          if delta >= -1000:
188              graphs.append(html.H3(f"Delta: {delta:.3f}"))
189          else:
190              graphs.append(html.H3("No laps recorded"))
191  
192      for segment in segments:
193          sector = segment.telemetry
194          fig = lap_fig(sector, columns=["Throttle", "Brake"])
195          brake_features = segment.brake_features()
196          throttle_features = segment.throttle_features()
197          if brake_features:
198              fig_add_features(fig, brake_features)
199          if throttle_features:
200              fig_add_features(fig, throttle_features, color="green")
201  
202          title = f"{segment.time:.3f} seconds - {segment.start}m to {segment.end}m"
203          fig.update_layout(title=dict(text=title))
204          graph = dcc.Graph(figure=fig)
205          md = get_segment_header(segment, segment.turn)
206          graphs.append(dcc.Markdown(md))
207          graphs.append(graph)
208  
209          if True:
210              # graphs.append(html.Hr())
211              # header = html.H6(f"type: {segment.type}")
212              # graphs.append(header)
213              brake_features = segment.brake_features()
214              throttle_features = segment.throttle_features()
215              data = [brake_features, throttle_features]
216              df = pd.DataFrame.from_records(data, index=["brake", "throttle"])
217              table = dbc.Table.from_dataframe(df, striped=True, bordered=True, hover=True, index=True, size="sm")
218              graphs.append(table)
219              gear_features = segment.gear_features()
220              data = [gear_features["distance_gear"]]
221              df = pd.DataFrame.from_records(data, index=["gear"])
222              table = dbc.Table.from_dataframe(df, striped=True, bordered=True, hover=True, index=True, size="sm")
223              graphs.append(table)
224              data = [
225                  {
226                      "brake / throttle": segment.brake_point() or segment.throttle_point(),
227                      "force": segment.brake_force() or segment.throttle_force(),
228                      "turn_in": segment.turn_in(),
229                      "apex": segment.apex(),
230                      "gear": segment.gear(),
231                  }
232              ]
233              df = pd.DataFrame.from_records(data, index=["markers"])
234              table = dbc.Table.from_dataframe(df, striped=True, bordered=True, hover=True, index=True, size="sm")
235              graphs.append(table)
236              # tb = html.Div(f"tb: {segment.trail_brake()} - {segment._tb_reason}")
237              # sector from {segment.start} to {segment.end}
238              # graphs.append(tb)
239  
240              if driver is not None:
241                  graphs.append(html.Hr())
242                  graphs.append(html.Pre(f"Driver delta: {segment.driver_delta():.3f}"))
243  
244              graphs.append(html.Hr())
245  
246      info = (
247          f"Based on a lap time of { history.lap_time_human() } extracted from { history.fast_lap.laps.count() } laps - "
248      )
249      laps_count = racing_stats.laps(game=game, track=track, car=car, valid=True).count()
250      info += f"Valid laps: {laps_count}"
251  
252      return graphs, info
253  
254  
255  def get_segment_header(segment, turn):
256      message = MessageTrackGuide(segment)
257      msg = message.msg
258  
259      md = f"""
260  ## Turn {turn}
261  {message.at} meters: {msg} - read_frame: {message.max_distance - message.at} meters
262  """
263  
264      return md