|
- 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'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
- f'<svg width="{svg_width}px" height="{svg_height}px" viewBox="0 0 {svg_width} {svg_height}" '
- 'xmlns="http://www.w3.org/2000/svg">',
- f' <title>{self.circuit.name} - {self.circuit.locality.name}</title>'
- ]
-
- # 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' <path d="{" ".join(path_data)}" fill="none" stroke="black" stroke-width="2"/>')
- svg.append('</svg>')
-
- # 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}")
|