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

136 行
4.4KB

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