F1 circuit layouts with year-by-year SVGs — manually traced track variations
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

121 行
5.3KB

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