F1 circuit layouts with year-by-year SVGs — manually traced track variations
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

211 行
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()