import json import os from dataclasses import dataclass from pathlib import Path from typing import Optional from models.geo_json.geo_json import GeoJSON from typing import TYPE_CHECKING if TYPE_CHECKING: from models.circuit import Circuit @dataclass class TrackLayout: slug: str description: str imageUrl: Optional[str] circuit: 'Circuit' _geo_data: Optional[GeoJSON] = None @classmethod def from_dict(cls, circuit: 'Circuit', slug: str, data: dict) -> 'TrackLayout': return cls( slug=slug, description=data["description"], imageUrl=data["imageUrl"], circuit=circuit ) @property def coordinates(self) -> list[tuple[float, float]]: return self._geo_data.features[0].geometry.coordinates def _relative_filepath(self, extension: str) -> Path: return Path(f"{self.circuit.locality.country.slug}/" f"{self.circuit.locality.slug}/" f"{self.circuit.slug}/" f"{self.slug}.{extension}") @property def relative_geojson_filepath(self) -> Optional[Path]: return self._relative_filepath("geo.json") @property def relative_svg_filepath(self) -> Optional[Path]: return self._relative_filepath("svg") @property def relative_png_filepath(self) -> Optional[Path]: return self._relative_filepath("png") def load_geo_json_data(self): """Load GeoJSON data for this layout""" file_path = Path(f"./circuits").joinpath(self.relative_geojson_filepath) if not file_path.exists(): return None try: with open(file_path, 'r') as f: data = json.load(f) self._geo_data = GeoJSON.from_dict(self.circuit, data) except Exception as e: print(f"Error loading GeoJSON data: {e}") return None def save_svg(self, path: Path, default_width: int=150, aspect_ratio: float=1.6): if not self.coordinates: print(f"No coordinates available for {self.circuit.name}") return # Extract coordinates lons, lats = zip(*self.coordinates) # Calculate canvas dimensions based on provided parameters svg_width = default_width svg_height = int(svg_width / aspect_ratio) # Higher aspect ratio = wider rectangle # Normalize coordinates to fit in SVG while maintaining proportions min_lon, max_lon = min(lons), max(lons) min_lat, max_lat = min(lats), max(lats) # Add padding lon_padding = (max_lon - min_lon) * 0.05 lat_padding = (max_lat - min_lat) * 0.05 min_lon -= lon_padding max_lon += lon_padding min_lat -= lat_padding max_lat += lat_padding # Calculate scaling factors for both dimensions lon_scale = svg_width / (max_lon - min_lon) lat_scale = svg_height / (max_lat - min_lat) # Use the smaller scaling factor to ensure the track fits within bounds scale = min(lon_scale, lat_scale) # Calculate centering offsets lon_offset = (svg_width - (max_lon - min_lon) * scale) / 2 lat_offset = (svg_height - (max_lat - min_lat) * scale) / 2 # Create SVG header svg = [ f'', f'', f' {self.circuit.name} - {self.circuit.locality.name}' ] # Create path data path_data = [] for i, (lon, lat) in enumerate(self.coordinates): # Scale and center the coordinates x = ((lon - min_lon) * scale) + lon_offset y = svg_height - ((lat - min_lat) * scale) - lat_offset if i == 0: path_data.append(f"M{x:.2f},{y:.2f}") else: path_data.append(f"L{x:.2f},{y:.2f}") # Add path to SVG svg.append(f' ') svg.append('') # Write SVG to file svg_path = path.joinpath(self.relative_svg_filepath) os.makedirs(os.path.dirname(svg_path), exist_ok=True) with open(svg_path, 'w', encoding='utf-8') as f: f.write('\n'.join(svg)) print(f"SVG saved to: {self.relative_svg_filepath}")