import fastf1 import numpy as np import matplotlib.pyplot as plt import os import json from datetime import datetime import re import fastf1.core as f1 # Enable cache to speed up subsequent data loading fastf1.Cache.enable_cache('cache') def rotate(xy, *, angle): """Rotate coordinates by the given angle.""" rot_mat = np.array([[np.cos(angle), np.sin(angle)], [-np.sin(angle), np.cos(angle)]]) return np.matmul(xy, rot_mat) def get_track_coordinates(session: f1.Session): """Extract track coordinates from session data.""" try: # Get the fastest lap and position data lap = session.laps.pick_fastest() pos = lap.get_pos_data() # Get circuit information circuit_info = session.get_circuit_info() # Print available attributes for debugging print("\nAvailable circuit_info attributes:") for attr in dir(circuit_info): if not attr.startswith('_'): # Skip private attributes try: value = getattr(circuit_info, attr) print(f" {attr}: {value}") except Exception as e: print(f" {attr}: Error accessing ({str(e)})") # Get track coordinates track = pos.loc[:, ('X', 'Y')].to_numpy() # Convert the rotation angle from degrees to radian track_angle = circuit_info.rotation / 180 * np.pi # Rotate the track coordinates rotated_track = rotate(track, angle=track_angle) # Create circuit_info dictionary with safe attribute access circuit_info_dict = { 'name': session.event['EventName'], 'country': session.event['Country'], 'city': session.event['Location'], 'length': getattr(circuit_info, 'length', None), 'rotation': getattr(circuit_info, 'rotation', None), } # Create event_info dictionary event_info_dict = { 'name': session.event['EventName'], 'year': session.event['EventDate'].year, 'official_name': session.event.get('OfficialEventName', session.event['EventName']), 'location': session.event['Location'] } return { 'coordinates': rotated_track.tolist(), 'circuit_info': circuit_info_dict, 'event_info': event_info_dict } except Exception as e: print(f"Error extracting track coordinates: {str(e)}") import traceback traceback.print_exc() return None def create_geojson(track_data): """Create a GeoJSON representation of the track.""" # Extract data from track_data coordinates = track_data['coordinates'] circuit_info = track_data['circuit_info'] event_info = track_data['event_info'] # Create a simplified ID based on country and year country_code = get_country_code(circuit_info['country']) year = event_info['year'] circuit_id = f"{country_code.lower()}-{year}" # Ensure length is an integer if available, or None if not circuit_length = None if circuit_info.get('length'): try: circuit_length = int(circuit_info['length']) except (ValueError, TypeError): pass # Create GeoJSON structure geojson = { "type": "FeatureCollection", "name": circuit_id, "features": [ { "type": "Feature", "properties": { "id": circuit_id, "Location": event_info['location'], "Name": circuit_info['name'], "opened": None, # Not available from API "firstgp": None, # Not available from API "length": circuit_length, "altitude": None # Not available from API }, "geometry": { "type": "LineString", "coordinates": coordinates } } ] } # Calculate bounding box if coordinates are available if coordinates and len(coordinates) > 0: x_coords = [coord[0] for coord in coordinates] y_coords = [coord[1] for coord in coordinates] bbox = [min(x_coords), min(y_coords), max(x_coords), max(y_coords)] # Add bounding box to GeoJSON geojson["bbox"] = bbox geojson["features"][0]["bbox"] = bbox return geojson def get_country_code(country_name): """Get a two-letter country code from a country name.""" country_codes = { "Australia": "AU", "Austria": "AT", "Azerbaijan": "AZ", "Bahrain": "BH", "Belgium": "BE", "Brazil": "BR", "Canada": "CA", "China": "CN", "Denmark": "DK", "France": "FR", "Germany": "DE", "Hungary": "HU", "Italy": "IT", "Japan": "JP", "Mexico": "MX", "Monaco": "MC", "Netherlands": "NL", "Portugal": "PT", "Qatar": "QA", "Russia": "RU", "Saudi Arabia": "SA", "Singapore": "SG", "Spain": "ES", "Sweden": "SE", "Switzerland": "CH", "United Arab Emirates": "AE", "United Kingdom": "GB", "United States": "US", "USA": "US", "Abu Dhabi": "AE", # Special case for events "Great Britain": "GB", "UK": "GB", "Emilia Romagna": "IT", # Special case for events "Styria": "AT", # Special case for events "Miami": "US", "Las Vegas": "US" } return country_codes.get(country_name, "XX") def sanitize_filename(name): """Convert a string to a valid filename.""" # Replace special characters with spaces name = re.sub(r'[\\/*?:"<>|]', ' ', name) # Replace multiple spaces with single space name = re.sub(r'\s+', ' ', name).strip() return name def save_svg(coordinates, output_path): """Generate and save an SVG representation of the circuit.""" if not coordinates: print("No coordinates available to create SVG") return False # Extract x and y coordinates x_coords = [coord[0] for coord in coordinates] y_coords = [coord[1] for coord in coordinates] # Normalize coordinates to fit in SVG min_x, max_x = min(x_coords), max(x_coords) min_y, max_y = min(y_coords), max(y_coords) # Add some padding x_padding = (max_x - min_x) * 0.05 y_padding = (max_y - min_y) * 0.05 min_x -= x_padding max_x += x_padding min_y -= y_padding max_y += y_padding # SVG dimensions svg_width = 800 svg_height = int(svg_width * (max_y - min_y) / (max_x - min_x)) # Create SVG header svg = [ f'', f'', ] # Create path data path_data = [] for i, (x, y) in enumerate(coordinates): # Convert coordinates to SVG coordinates svg_x = svg_width * (x - min_x) / (max_x - min_x) # Flip y-axis because SVG has origin at top-left svg_y = svg_height * (1 - (y - min_y) / (max_y - min_y)) if i == 0: path_data.append(f"M{svg_x:.2f},{svg_y:.2f}") else: path_data.append(f"L{svg_x:.2f},{svg_y:.2f}") # Add path to SVG svg.append(f' ') svg.append('') # Write SVG to file os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write('\n'.join(svg)) return True def save_png(coordinates, output_path): """Generate and save a PNG representation of the circuit.""" if not coordinates: print("No coordinates available to create PNG") return False # Extract x and y coordinates x_coords = [coord[0] for coord in coordinates] y_coords = [coord[1] for coord in coordinates] # Create figure with transparent background fig = plt.figure(figsize=(10, 8), facecolor="none") ax = fig.add_subplot(111) # Plot the circuit with black line ax.plot(x_coords, y_coords, 'k-', linewidth=2) # Remove grid, axes, and border ax.grid(False) ax.set_xticks([]) ax.set_yticks([]) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['bottom'].set_visible(False) ax.spines['left'].set_visible(False) # Keep aspect ratio equal ax.set_aspect('equal') # Add padding around the circuit ax.margins(0.1) # Save with transparent background os.makedirs(os.path.dirname(output_path), exist_ok=True) fig.savefig(output_path, transparent=True, bbox_inches='tight', pad_inches=0.1) plt.close(fig) return True def get_available_seasons(): """Get a list of available F1 seasons.""" current_year = datetime.now().year # F1 API has data from 1950 to current year return list(range(1950, current_year + 1)) def prompt_season_selection(): """Prompt the user to select a season.""" seasons = get_available_seasons() print("\nAvailable F1 Seasons:") # Display seasons in rows of 10 for i in range(0, len(seasons), 10): row = seasons[i:i+10] print(" ".join(f"{year:<6}" for year in row)) while True: try: selection = input("\nEnter season year (e.g., 2023): ") year = int(selection) if year in seasons: return year else: print(f"Invalid season: {year}. Please choose a year between 1950 and {seasons[-1]}.") except ValueError: print("Please enter a valid year (e.g., 2023).") def get_events_for_season(year): """Get all events for a specific season.""" try: schedule = fastf1.get_event_schedule(year) return schedule except Exception as e: print(f"Error fetching events for {year}: {str(e)}") return None def prompt_event_selection(events): """Prompt the user to select an event from the schedule.""" if events is None or len(events) == 0: print("No events available for the selected season.") return None print("\nAvailable Grands Prix:") for i, (_, event) in enumerate(events.iterrows(), 1): event_name = event['EventName'] location = event['Location'] date = event['EventDate'].strftime('%Y-%m-%d') print(f"{i:2}. {event_name:<35} {location:<20} {date}") while True: try: selection = input("\nEnter the number of the Grand Prix: ") idx = int(selection) - 1 if 0 <= idx < len(events): return events.iloc[idx] else: print(f"Invalid selection. Please choose a number between 1 and {len(events)}.") except ValueError: print("Please enter a valid number.") def create_output_directory(country, location, circuit_name, year): """Create the appropriate directory structure for saving files.""" # Sanitize names for directory structure country = sanitize_filename(country) location = sanitize_filename(location) circuit_name = sanitize_filename(circuit_name) # Create directory path base_dir = os.path.join("circuits", country, location) os.makedirs(base_dir, exist_ok=True) # Create filename base (without extension) filename_base = f"{circuit_name} - {year}-" return base_dir, filename_base def main(): try: print("F1 Circuit Coordinate Generator") print("===============================") print("This tool allows you to download coordinates for F1 circuits and save them as GeoJSON, SVG, and PNG files.") # Step 1: Select a season year = prompt_season_selection() print(f"\nSelected season: {year}") # Step 2: Get events for the selected season print(f"\nFetching events for {year}...") events = get_events_for_season(year) if events is None or len(events) == 0: print(f"No events found for the {year} season. Exiting.") return # Step 3: Select an event event = prompt_event_selection(events) if event is None: print("No event selected. Exiting.") return print(f"\nSelected event: {event['EventName']} at {event['Location']}") # Step 4: Download the session data (try qualifying first, then other sessions) print("\nDownloading session data (this may take a moment)...") session_types = ['Q', 'FP1', 'FP2', 'FP3', 'R'] session = None for session_type in session_types: try: print(f"Trying to load {session_type} session...") session = fastf1.get_session(year, event['EventName'], session_type) session.load() if session and session.total_laps is not None and session.total_laps > 0: print(f"Successfully loaded {session_type} session") break except Exception as e: print(f"Could not load {session_type} session: {str(e)}") if session is None: print("Could not load any session data. Exiting.") return # Step 5: Extract track coordinates print("\nExtracting track coordinates...") track_data = get_track_coordinates(session) if not track_data: print("Failed to extract track coordinates. Exiting.") return # Step 6: Create GeoJSON data print("\nCreating GeoJSON data...") geojson_data = create_geojson(track_data) # Step 7: Determine output directory and filenames circuit_info = track_data['circuit_info'] event_info = track_data['event_info'] country = circuit_info['country'] location = event_info['location'] circuit_name = circuit_info['name'] year = event_info['year'] print(f"\nPreparing to save circuit: {circuit_name} ({location}, {country}) from {year}") base_dir, filename_base = create_output_directory(country, location, circuit_name, year) geojson_path = os.path.join(base_dir, f"{filename_base}.geo.json") svg_path = os.path.join(base_dir, f"{filename_base}.svg") png_path = os.path.join(base_dir, f"{filename_base}.png") # Step 8: Save files print("\nSaving files...") # Save GeoJSON with open(geojson_path, 'w', encoding='utf-8') as f: json.dump(geojson_data, f, indent=2) print(f"Saved GeoJSON to: {geojson_path}") # Save SVG if save_svg(track_data['coordinates'], svg_path): print(f"Saved SVG to: {svg_path}") # Save PNG if save_png(track_data['coordinates'], png_path): print(f"Saved PNG to: {png_path}") print("\nDone! Files have been saved successfully.") except Exception as e: print(f"\nAn error occurred: {str(e)}") import traceback traceback.print_exc() if __name__ == "__main__": main()