F1 circuit layouts with year-by-year SVGs — manually traced track variations
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

136 rindas
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}")