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

143 行
4.6KB

  1. import json
  2. import os
  3. from dataclasses import dataclass
  4. from pathlib import Path
  5. from typing import Optional
  6. from models.geo_json.geo_json import GeoJSON
  7. from typing import TYPE_CHECKING
  8. if TYPE_CHECKING:
  9. from models.circuit import Circuit
  10. @dataclass
  11. class TrackLayout:
  12. slug: str
  13. description: str
  14. imageUrl: Optional[str]
  15. circuit: 'Circuit'
  16. _geo_data: Optional[GeoJSON] = None
  17. @classmethod
  18. def from_dict(cls, circuit: 'Circuit', slug: str, data: dict) -> 'TrackLayout':
  19. return cls(
  20. slug=slug,
  21. description=data["description"],
  22. imageUrl=data["imageUrl"],
  23. circuit=circuit
  24. )
  25. def to_dict(self) -> dict:
  26. return {
  27. "slug": self.slug,
  28. "description": self.description,
  29. "imageUrl": self.imageUrl,
  30. }
  31. @property
  32. def coordinates(self) -> list[tuple[float, float]]:
  33. return self._geo_data.features[0].geometry.coordinates
  34. def _relative_filepath(self, extension: str) -> Path:
  35. return Path(f"{self.circuit.locality.country.slug}/"
  36. f"{self.circuit.locality.slug}/"
  37. f"{self.circuit.slug}/"
  38. f"{self.slug}.{extension}")
  39. @property
  40. def relative_geojson_filepath(self) -> Optional[Path]:
  41. return self._relative_filepath("geo.json")
  42. @property
  43. def relative_svg_filepath(self) -> Optional[Path]:
  44. return self._relative_filepath("svg")
  45. @property
  46. def relative_png_filepath(self) -> Optional[Path]:
  47. return self._relative_filepath("png")
  48. def load_geo_json_data(self):
  49. """Load GeoJSON data for this layout"""
  50. file_path = Path(f"./circuits").joinpath(self.relative_geojson_filepath)
  51. if not file_path.exists():
  52. return None
  53. try:
  54. with open(file_path, 'r') as f:
  55. data = json.load(f)
  56. self._geo_data = GeoJSON.from_dict(self.circuit, data)
  57. except Exception as e:
  58. print(f"Error loading GeoJSON data: {e}")
  59. return None
  60. def save_svg(self, path: Path, default_width: int=150, aspect_ratio: float=1.6):
  61. if not self.coordinates:
  62. print(f"No coordinates available for {self.circuit.name}")
  63. return
  64. # Extract coordinates
  65. lons, lats = zip(*self.coordinates)
  66. # Calculate canvas dimensions based on provided parameters
  67. svg_width = default_width
  68. svg_height = int(svg_width / aspect_ratio) # Higher aspect ratio = wider rectangle
  69. # Normalize coordinates to fit in SVG while maintaining proportions
  70. min_lon, max_lon = min(lons), max(lons)
  71. min_lat, max_lat = min(lats), max(lats)
  72. # Add padding
  73. lon_padding = (max_lon - min_lon) * 0.05
  74. lat_padding = (max_lat - min_lat) * 0.05
  75. min_lon -= lon_padding
  76. max_lon += lon_padding
  77. min_lat -= lat_padding
  78. max_lat += lat_padding
  79. # Calculate scaling factors for both dimensions
  80. lon_scale = svg_width / (max_lon - min_lon)
  81. lat_scale = svg_height / (max_lat - min_lat)
  82. # Use the smaller scaling factor to ensure the track fits within bounds
  83. scale = min(lon_scale, lat_scale)
  84. # Calculate centering offsets
  85. lon_offset = (svg_width - (max_lon - min_lon) * scale) / 2
  86. lat_offset = (svg_height - (max_lat - min_lat) * scale) / 2
  87. # Create SVG header
  88. svg = [
  89. f'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
  90. f'<svg width="{svg_width}px" height="{svg_height}px" viewBox="0 0 {svg_width} {svg_height}" '
  91. 'xmlns="http://www.w3.org/2000/svg">',
  92. f' <title>{self.circuit.name} - {self.circuit.locality.name}</title>'
  93. ]
  94. # Create path data
  95. path_data = []
  96. for i, (lon, lat) in enumerate(self.coordinates):
  97. # Scale and center the coordinates
  98. x = ((lon - min_lon) * scale) + lon_offset
  99. y = svg_height - ((lat - min_lat) * scale) - lat_offset
  100. if i == 0:
  101. path_data.append(f"M{x:.2f},{y:.2f}")
  102. else:
  103. path_data.append(f"L{x:.2f},{y:.2f}")
  104. # Add path to SVG
  105. svg.append(f' <path d="{" ".join(path_data)}" fill="none" stroke="black" stroke-width="2"/>')
  106. svg.append('</svg>')
  107. # Write SVG to file
  108. svg_path = path.joinpath(self.relative_svg_filepath)
  109. os.makedirs(os.path.dirname(svg_path), exist_ok=True)
  110. with open(svg_path, 'w', encoding='utf-8') as f:
  111. f.write('\n'.join(svg))
  112. print(f"SVG saved to: {self.relative_svg_filepath}")