F1 circuit layouts with year-by-year SVGs — manually traced track variations
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

127 lignes
5.4KB

  1. import json
  2. import logging
  3. from typing import Optional
  4. import functools
  5. from enum import Enum
  6. # Import FastF1 for dynamic circuit data
  7. import fastf1
  8. from ergast_service import ErgastService
  9. from models.circuit import Circuit
  10. from models.country import Country
  11. from models.locality import Locality
  12. from models.model_builder import parse_circuits_json
  13. from models.track_layout import TrackLayout
  14. # Define supported formats
  15. class Format(str, Enum):
  16. PNG = "png"
  17. SVG = "svg"
  18. GEOJSON = "geojson"
  19. class CircuitService:
  20. def __init__(self, circuit_data_path: str = "circuits/circuits.json"):
  21. self.circuit_data_path = circuit_data_path
  22. self.circuit_data = self._load_circuit_data()
  23. self._layout_cache = {}
  24. self._circuit_file_cache = {}
  25. self._grand_prix_cache = {}
  26. self.ergast_service = ErgastService(self)
  27. # Configure logging
  28. logging.basicConfig(level=logging.INFO)
  29. self.logger = logging.getLogger(__name__)
  30. # Initialize FastF1 cache
  31. fastf1.Cache.enable_cache('cache')
  32. @functools.lru_cache()
  33. def _load_circuit_data(self) -> dict[str, Country]:
  34. """Load and cache circuit data"""
  35. with open(self.circuit_data_path) as f:
  36. return parse_circuits_json(json.load(f))
  37. def find_layout_slug_for_year(self, circuit: Circuit, year: int) -> Optional[str]:
  38. """Find the appropriate layout slug for a given year"""
  39. cache_key = f"{circuit.name}-{year}"
  40. if cache_key in self._layout_cache:
  41. return self._layout_cache[cache_key]
  42. for layout_slug, _ in circuit.layouts.items():
  43. # Check different layout slug formats
  44. if '-' in layout_slug:
  45. # Handle range format
  46. start_year, *end = layout_slug.split('-')
  47. start_year = int(start_year)
  48. if not end[0]:
  49. # Format: 1967- (no end year)
  50. if year >= start_year:
  51. self._layout_cache[cache_key] = layout_slug
  52. return layout_slug
  53. else:
  54. # Format: 1967-1968
  55. end_year = int(end[0])
  56. if start_year <= year <= end_year:
  57. self._layout_cache[cache_key] = layout_slug
  58. return layout_slug
  59. else:
  60. # Format: 1967 (single year)
  61. if year == int(layout_slug):
  62. self._layout_cache[cache_key] = layout_slug
  63. return layout_slug
  64. return None
  65. def get_countries_list(self) -> Optional[list[Country]]:
  66. """Get list of all available countries with their slugs"""
  67. return [country for _, country in self.circuit_data.items()]
  68. def get_country_details(self, country_slug: str) -> Optional[Country]:
  69. """Get details for a specific country"""
  70. return self.circuit_data.get(country_slug)
  71. def get_localities_list(self, country_slug: str) -> Optional[list[Locality]]:
  72. """Get list of cities for a specific country"""
  73. return [locality for _, locality in self.circuit_data[country_slug].localities.items()]
  74. def get_locality_details(self, country_slug: str, locality_slug: str) -> Optional[Locality]:
  75. """Get details for a specific city"""
  76. return self.circuit_data[country_slug].localities.get(locality_slug)
  77. def get_circuits_list(self, country_slug: str, locality_slug: str) -> Optional[list[Circuit]]:
  78. """Get list of circuits for a specific city"""
  79. return [circuit_data for _, circuit_data in self.circuit_data[country_slug].localities.get(locality_slug).circuits.items()]
  80. def get_circuit_details(self, country_slug: str, locality_slug: str, circuit_slug: str) -> Optional[Circuit]:
  81. """Get circuit details and available formats"""
  82. return self.circuit_data[country_slug].localities.get(locality_slug).circuits.get(circuit_slug)
  83. def get_layout_details(self, country_slug: str, locality_slug: str, circuit_slug: str, layout_slug: str) -> Optional[TrackLayout]:
  84. """Get details for a specific layout"""
  85. return self.circuit_data[country_slug].localities.get(locality_slug).circuits.get(circuit_slug).layouts.get(
  86. layout_slug)
  87. def get_circuit_layout_by_ergast_data(self, grand_prix_name: str, season: int) -> Optional[TrackLayout]:
  88. """
  89. Find and return the circuit layout file path based on Ergast F1 API race data format.
  90. """
  91. country_slug, locality_slug, circuit_slug = self.ergast_service.find_slugs_for_grand_prix(grand_prix_name, season)
  92. if country_slug is None or locality_slug is None or circuit_slug is None:
  93. self.logger.error(f"Error finding circuit layout in Ergast for grand prix {grand_prix_name} in season {season}")
  94. return None
  95. circuit = self.get_circuit_details(country_slug, locality_slug, circuit_slug)
  96. if circuit is None:
  97. self.logger.error(f"Error finding circuit details for Ergast country, locality and circuit slug: {country_slug}, {locality_slug}, {circuit_slug}")
  98. return None
  99. layout_slug = self.find_layout_slug_for_year(circuit, season)
  100. if layout_slug is None:
  101. self.logger.error(f"Error finding layout slug for year {season} for circuit {circuit_slug}")
  102. return None
  103. return self.get_layout_details(country_slug, locality_slug, circuit_slug, layout_slug)