Dataclass models have parent back-references (TrackLayout→Circuit→ Locality→Country) causing infinite recursion in dataclasses.asdict(). Add to_dict() methods that exclude parent refs, use them in API endpoints.
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
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
|
|
)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"slug": self.slug,
|
|
"description": self.description,
|
|
"imageUrl": self.imageUrl,
|
|
}
|
|
|
|
@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}")
|