F1 circuit layouts with year-by-year SVGs — manually traced track variations
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

211 Zeilen
6.9KB

  1. import os
  2. import json
  3. import matplotlib.pyplot as plt
  4. from dataclasses import dataclass
  5. from typing import List, Dict, Any, Optional, Tuple
  6. @dataclass
  7. class Circuit:
  8. """Model class representing an F1 circuit."""
  9. id: str
  10. name: str
  11. location: str
  12. opened: Optional[int] = None
  13. first_gp: Optional[int] = None
  14. length: Optional[int] = None # in meters
  15. altitude: Optional[int] = None
  16. coordinates: List[Tuple[float, float]] = None
  17. bbox: Optional[List[float]] = None
  18. def __post_init__(self):
  19. if self.coordinates is None:
  20. self.coordinates = []
  21. def plot(self):
  22. """Generate a simple plot of the circuit with transparent background and no grid/axes."""
  23. if not self.coordinates:
  24. print(f"No coordinates available for {self.name}")
  25. return
  26. # Extract longitude and latitude from coordinates
  27. lons, lats = zip(*self.coordinates)
  28. # Create figure with transparent background
  29. fig = plt.figure(figsize=(10, 8), facecolor="none")
  30. ax = fig.add_subplot(111)
  31. # Plot the circuit with black line
  32. ax.plot(lons, lats, 'k-', linewidth=2)
  33. # Remove grid, axes, and title
  34. ax.grid(False)
  35. ax.set_xticks([])
  36. ax.set_yticks([])
  37. ax.spines['top'].set_visible(False)
  38. ax.spines['right'].set_visible(False)
  39. ax.spines['bottom'].set_visible(False)
  40. ax.spines['left'].set_visible(False)
  41. # Keep aspect ratio equal
  42. ax.set_aspect('equal')
  43. # Add padding around the circuit
  44. ax.margins(0.1)
  45. # Tight layout
  46. plt.tight_layout()
  47. return fig
  48. def parse_geojson(file_path: str) -> Optional[Circuit]:
  49. """Parse a GeoJSON file and return a Circuit object."""
  50. try:
  51. with open(file_path, 'r', encoding='utf-8') as f:
  52. data = json.load(f)
  53. # Get the first feature (typically there's only one per file)
  54. if not data.get('features'):
  55. print(f"No features found in {file_path}")
  56. return None
  57. feature = data['features'][0]
  58. properties = feature.get('properties', {})
  59. geometry = feature.get('geometry', {})
  60. # Extract coordinates
  61. coordinates = []
  62. if geometry.get('type') == 'LineString':
  63. coordinates = geometry.get('coordinates', [])
  64. # Create Circuit object
  65. circuit = Circuit(
  66. id=properties.get('id', ''),
  67. name=properties.get('Name', ''),
  68. location=properties.get('Location', ''),
  69. opened=properties.get('opened'),
  70. first_gp=properties.get('firstgp'),
  71. length=properties.get('length'),
  72. altitude=properties.get('altitude'),
  73. coordinates=coordinates,
  74. bbox=feature.get('bbox') or data.get('bbox')
  75. )
  76. return circuit
  77. except Exception as e:
  78. print(f"Error parsing {file_path}: {str(e)}")
  79. return None
  80. def save_svg(circuit: Circuit, output_path: str):
  81. """Generate and save an SVG representation of the circuit."""
  82. if not circuit.coordinates:
  83. print(f"No coordinates available for {circuit.name}")
  84. return
  85. # Extract coordinates
  86. lons, lats = zip(*circuit.coordinates)
  87. # Normalize coordinates to fit in SVG
  88. min_lon, max_lon = min(lons), max(lons)
  89. min_lat, max_lat = min(lats), max(lats)
  90. # Add some padding
  91. lon_padding = (max_lon - min_lon) * 0.05
  92. lat_padding = (max_lat - min_lat) * 0.05
  93. min_lon -= lon_padding
  94. max_lon += lon_padding
  95. min_lat -= lat_padding
  96. max_lat += lat_padding
  97. # SVG dimensions
  98. svg_width = 800
  99. svg_height = int(svg_width * (max_lat - min_lat) / (max_lon - min_lon))
  100. # Create SVG header
  101. svg = [
  102. f'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
  103. f'<svg width="{svg_width}" height="{svg_height}" viewBox="0 0 {svg_width} {svg_height}" xmlns="http://www.w3.org/2000/svg">',
  104. f' <title>{circuit.name} - {circuit.location}</title>'
  105. ]
  106. # Create path data
  107. path_data = []
  108. for i, (lon, lat) in enumerate(circuit.coordinates):
  109. # Convert geo coordinates to SVG coordinates
  110. x = svg_width * (lon - min_lon) / (max_lon - min_lon)
  111. # Flip y-axis because SVG has origin at top-left
  112. y = svg_height * (1 - (lat - min_lat) / (max_lat - min_lat))
  113. if i == 0:
  114. path_data.append(f"M{x:.2f},{y:.2f}")
  115. else:
  116. path_data.append(f"L{x:.2f},{y:.2f}")
  117. # Add path to SVG
  118. svg.append(f' <path d="{" ".join(path_data)}" fill="none" stroke="black" stroke-width="2"/>')
  119. svg.append('</svg>')
  120. # Write SVG to file
  121. os.makedirs(os.path.dirname(output_path), exist_ok=True)
  122. with open(output_path, 'w', encoding='utf-8') as f:
  123. f.write('\n'.join(svg))
  124. print(f"SVG saved to: {output_path}")
  125. def main():
  126. circuits_dir = "./"
  127. # List to store all .geo.json files
  128. geo_json_files = []
  129. # Walk through all subdirectories
  130. for root, dirs, files in os.walk(circuits_dir):
  131. for file in files:
  132. # Check if file ends with .geo.json
  133. if file.endswith(".geo.json"):
  134. file_path = os.path.join(root, file)
  135. geo_json_files.append(file_path)
  136. # List to store all parsed circuits
  137. circuits = []
  138. # Process each .geo.json file
  139. for geo_json_file in geo_json_files:
  140. print(f"Processing: {geo_json_file}")
  141. # Parse the GeoJSON file
  142. circuit = parse_geojson(geo_json_file)
  143. if circuit:
  144. circuits.append(circuit)
  145. # Print circuit info
  146. print(f" Circuit: {circuit.name} ({circuit.location})")
  147. print(f" ID: {circuit.id}")
  148. print(f" Length: {circuit.length}m, Altitude: {circuit.altitude}m")
  149. print(f" Coordinates: {len(circuit.coordinates)} points")
  150. # Generate output file paths in the same directory as the GeoJSON
  151. file_dir = os.path.dirname(geo_json_file)
  152. file_basename = os.path.basename(geo_json_file).replace('.geo.json', '')
  153. svg_path = os.path.join(file_dir, f"{file_basename}.svg")
  154. png_path = os.path.join(file_dir, f"{file_basename}.png")
  155. # Generate SVG
  156. save_svg(circuit, svg_path)
  157. # Generate PNG plot with transparent background
  158. fig = circuit.plot()
  159. if fig:
  160. fig.savefig(png_path, transparent=True, bbox_inches='tight', pad_inches=0.1)
  161. plt.close(fig)
  162. print(f"Plot saved to: {png_path}")
  163. else:
  164. print(f"Failed to parse {geo_json_file}")
  165. print(f"\nProcessed {len(circuits)} circuits")
  166. if __name__ == "__main__":
  167. main()