F1 circuit layouts with year-by-year SVGs — manually traced track variations
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

284 líneas
9.0KB

  1. import fastf1
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. import os
  5. import json
  6. import re
  7. from typing import List, Dict, Any, Optional, Tuple
  8. # Import for type hints
  9. import fastf1.core
  10. def rotate(xy, *, angle):
  11. """Rotate coordinates by the given angle."""
  12. rot_mat = np.array([[np.cos(angle), np.sin(angle)],
  13. [-np.sin(angle), np.cos(angle)]])
  14. return np.matmul(xy, rot_mat)
  15. def get_track_coordinates(session: fastf1.core.Session):
  16. """Extract track coordinates from session data."""
  17. try:
  18. # Get the fastest lap and position data
  19. lap = session.laps.pick_fastest()
  20. pos = lap.get_pos_data()
  21. # Get circuit information
  22. circuit_info = session.get_circuit_info()
  23. # Get track coordinates
  24. track = pos.loc[:, ('X', 'Y')].to_numpy()
  25. # Convert the rotation angle from degrees to radian
  26. track_angle = getattr(circuit_info, 'rotation', 0) / 180 * np.pi
  27. # Rotate the track coordinates
  28. rotated_track = rotate(track, angle=track_angle)
  29. # Create circuit_info dictionary with safe attribute access
  30. circuit_info_dict = {
  31. 'name': session.event['EventName'],
  32. 'country': session.event['Country'],
  33. 'city': session.event['Location'],
  34. 'length': getattr(circuit_info, 'length', None),
  35. 'rotation': getattr(circuit_info, 'rotation', None),
  36. }
  37. # Create event_info dictionary
  38. event_info_dict = {
  39. 'name': session.event['EventName'],
  40. 'year': session.event['EventDate'].year,
  41. 'official_name': session.event.get('OfficialEventName', session.event['EventName']),
  42. 'location': session.event['Location']
  43. }
  44. return {
  45. 'coordinates': rotated_track.tolist(),
  46. 'circuit_info': circuit_info_dict,
  47. 'event_info': event_info_dict
  48. }
  49. except Exception as e:
  50. print(f"Error extracting track coordinates: {str(e)}")
  51. import traceback
  52. traceback.print_exc()
  53. return None
  54. def create_geojson(track_data):
  55. """Create a GeoJSON representation of the track."""
  56. # Extract data from track_data
  57. coordinates = track_data['coordinates']
  58. circuit_info = track_data['circuit_info']
  59. event_info = track_data['event_info']
  60. # Create a simplified ID based on country and year
  61. country_code = get_country_code(circuit_info['country'])
  62. year = event_info['year']
  63. circuit_id = f"{country_code.lower()}-{year}"
  64. # Ensure length is an integer if available, or None if not
  65. circuit_length = None
  66. if circuit_info.get('length'):
  67. try:
  68. circuit_length = int(circuit_info['length'])
  69. except (ValueError, TypeError):
  70. pass
  71. # Create GeoJSON structure
  72. geojson = {
  73. "type": "FeatureCollection",
  74. "name": circuit_id,
  75. "features": [
  76. {
  77. "type": "Feature",
  78. "properties": {
  79. "id": circuit_id,
  80. "Location": event_info['location'],
  81. "Name": circuit_info['name'],
  82. "opened": None, # Not available from API
  83. "firstgp": None, # Not available from API
  84. "length": circuit_length,
  85. "altitude": None # Not available from API
  86. },
  87. "geometry": {
  88. "type": "LineString",
  89. "coordinates": coordinates
  90. }
  91. }
  92. ]
  93. }
  94. # Calculate bounding box if coordinates are available
  95. if coordinates and len(coordinates) > 0:
  96. x_coords = [coord[0] for coord in coordinates]
  97. y_coords = [coord[1] for coord in coordinates]
  98. bbox = [min(x_coords), min(y_coords), max(x_coords), max(y_coords)]
  99. # Add bounding box to GeoJSON
  100. geojson["bbox"] = bbox
  101. geojson["features"][0]["bbox"] = bbox
  102. return geojson
  103. def get_country_code(country_name):
  104. """Get a two-letter country code from a country name."""
  105. country_codes = {
  106. "Australia": "AU",
  107. "Austria": "AT",
  108. "Azerbaijan": "AZ",
  109. "Bahrain": "BH",
  110. "Belgium": "BE",
  111. "Brazil": "BR",
  112. "Canada": "CA",
  113. "China": "CN",
  114. "Denmark": "DK",
  115. "France": "FR",
  116. "Germany": "DE",
  117. "Hungary": "HU",
  118. "Italy": "IT",
  119. "Japan": "JP",
  120. "Mexico": "MX",
  121. "Monaco": "MC",
  122. "Netherlands": "NL",
  123. "Portugal": "PT",
  124. "Qatar": "QA",
  125. "Russia": "RU",
  126. "Saudi Arabia": "SA",
  127. "Singapore": "SG",
  128. "Spain": "ES",
  129. "Sweden": "SE",
  130. "Switzerland": "CH",
  131. "United Arab Emirates": "AE",
  132. "United Kingdom": "GB",
  133. "United States": "US",
  134. "USA": "US",
  135. "Abu Dhabi": "AE", # Special case for events
  136. "Great Britain": "GB",
  137. "UK": "GB",
  138. "Emilia Romagna": "IT", # Special case for events
  139. "Styria": "AT", # Special case for events
  140. "Miami": "US",
  141. "Las Vegas": "US"
  142. }
  143. return country_codes.get(country_name, "XX")
  144. def sanitize_filename(name):
  145. """Convert a string to a valid filename."""
  146. # Replace special characters with spaces
  147. name = re.sub(r'[\\/*?:"<>|]', ' ', name)
  148. # Replace multiple spaces with single space
  149. name = re.sub(r'\s+', ' ', name).strip()
  150. return name
  151. def save_svg(coordinates, output_path):
  152. """Generate and save an SVG representation of the circuit."""
  153. if not coordinates:
  154. print("No coordinates available to create SVG")
  155. return False
  156. # Extract x and y coordinates
  157. x_coords = [coord[0] for coord in coordinates]
  158. y_coords = [coord[1] for coord in coordinates]
  159. # Normalize coordinates to fit in SVG
  160. min_x, max_x = min(x_coords), max(x_coords)
  161. min_y, max_y = min(y_coords), max(y_coords)
  162. # Add some padding
  163. x_padding = (max_x - min_x) * 0.05
  164. y_padding = (max_y - min_y) * 0.05
  165. min_x -= x_padding
  166. max_x += x_padding
  167. min_y -= y_padding
  168. max_y += y_padding
  169. # SVG dimensions
  170. svg_width = 800
  171. svg_height = int(svg_width * (max_y - min_y) / (max_x - min_x))
  172. # Create SVG header
  173. svg = [
  174. f'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
  175. f'<svg width="{svg_width}" height="{svg_height}" viewBox="0 0 {svg_width} {svg_height}" xmlns="http://www.w3.org/2000/svg">',
  176. ]
  177. # Create path data
  178. path_data = []
  179. for i, (x, y) in enumerate(coordinates):
  180. # Convert coordinates to SVG coordinates
  181. svg_x = svg_width * (x - min_x) / (max_x - min_x)
  182. # Flip y-axis because SVG has origin at top-left
  183. svg_y = svg_height * (1 - (y - min_y) / (max_y - min_y))
  184. if i == 0:
  185. path_data.append(f"M{svg_x:.2f},{svg_y:.2f}")
  186. else:
  187. path_data.append(f"L{svg_x:.2f},{svg_y:.2f}")
  188. # Add path to SVG
  189. svg.append(f' <path d="{" ".join(path_data)}" fill="none" stroke="black" stroke-width="2"/>')
  190. svg.append('</svg>')
  191. # Write SVG to file
  192. os.makedirs(os.path.dirname(output_path), exist_ok=True)
  193. with open(output_path, 'w', encoding='utf-8') as f:
  194. f.write('\n'.join(svg))
  195. return True
  196. def save_png(coordinates, output_path):
  197. """Generate and save a PNG representation of the circuit."""
  198. if not coordinates:
  199. print("No coordinates available to create PNG")
  200. return False
  201. # Extract x and y coordinates
  202. x_coords = [coord[0] for coord in coordinates]
  203. y_coords = [coord[1] for coord in coordinates]
  204. # Create figure with transparent background
  205. fig = plt.figure(figsize=(10, 8), facecolor="none")
  206. ax = fig.add_subplot(111)
  207. # Plot the circuit with black line
  208. ax.plot(x_coords, y_coords, 'k-', linewidth=2)
  209. # Remove grid, axes, and border
  210. ax.grid(False)
  211. ax.set_xticks([])
  212. ax.set_yticks([])
  213. ax.spines['top'].set_visible(False)
  214. ax.spines['right'].set_visible(False)
  215. ax.spines['bottom'].set_visible(False)
  216. ax.spines['left'].set_visible(False)
  217. # Keep aspect ratio equal
  218. ax.set_aspect('equal')
  219. # Add padding around the circuit
  220. ax.margins(0.1)
  221. # Save with transparent background
  222. os.makedirs(os.path.dirname(output_path), exist_ok=True)
  223. fig.savefig(output_path, transparent=True, bbox_inches='tight', pad_inches=0.1)
  224. plt.close(fig)
  225. return True
  226. # Additional utility functions for interactive mode
  227. def get_available_seasons():
  228. """Get a list of available F1 seasons."""
  229. from datetime import datetime
  230. current_year = datetime.now().year
  231. # F1 API has data from 1950 to current year
  232. return list(range(1950, current_year + 1))
  233. def get_events_for_season(year):
  234. """Get all events for a specific season."""
  235. try:
  236. schedule = fastf1.get_event_schedule(year)
  237. return schedule
  238. except Exception as e:
  239. print(f"Error fetching events for {year}: {str(e)}")
  240. return None