F1 circuit layouts with year-by-year SVGs — manually traced track variations
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

143 linhas
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}")