import numpy as np import matplotlib.pyplot as plt data = { "type": "FeatureCollection", "name": "hu-1986", "bbox": [ 19.242326, 47.577571, 19.256609, 47.588474 ], "features": [ { "type": "Feature", "properties": { "id": "hu-1986", "Location": "Budapest", "Name": "Hungaroring", "opened": 1986, "firstgp": 1986, "length": 4381, "altitude": 239 }, "bbox": [ 19.242326, 47.577571, 19.256609, 47.588474 ], "geometry": { "type": "LineString", "coordinates": [ [ 19.245888, 47.58026 ], [ 19.243226, 47.581696 ], [ 19.242439, 47.582111 ], [ 19.242368, 47.582191 ], [ 19.242326, 47.582271 ], [ 19.242338, 47.582361 ], [ 19.242391, 47.582431 ], [ 19.242468, 47.582478 ], [ 19.242569, 47.582511 ], [ 19.242717, 47.582526 ], [ 19.242936, 47.582526 ], [ 19.243546, 47.582516 ], [ 19.243954, 47.582469 ], [ 19.244327, 47.582394 ], [ 19.244718, 47.58229 ], [ 19.245014, 47.582177 ], [ 19.245238, 47.582073 ], [ 19.247411, 47.580913 ], [ 19.24757, 47.580857 ], [ 19.247724, 47.580833 ], [ 19.247878, 47.580833 ], [ 19.248003, 47.580862 ], [ 19.248139, 47.580895 ], [ 19.248245, 47.580951 ], [ 19.248334, 47.581017 ], [ 19.248393, 47.581078 ], [ 19.248446, 47.581163 ], [ 19.248464, 47.581281 ], [ 19.248458, 47.581347 ], [ 19.248411, 47.581437 ], [ 19.24837, 47.581526 ], [ 19.247748, 47.582238 ], [ 19.247683, 47.58237 ], [ 19.247671, 47.582474 ], [ 19.247707, 47.582563 ], [ 19.247754, 47.582676 ], [ 19.247813, 47.58278 ], [ 19.2485, 47.583704 ], [ 19.249719, 47.585368 ], [ 19.249938, 47.585632 ], [ 19.250193, 47.585863 ], [ 19.250512, 47.586113 ], [ 19.250577, 47.586183 ], [ 19.250625, 47.586254 ], [ 19.250636, 47.586334 ], [ 19.250601, 47.586424 ], [ 19.250045, 47.587838 ], [ 19.250033, 47.587946 ], [ 19.25005, 47.58805 ], [ 19.250092, 47.588154 ], [ 19.250157, 47.588253 ], [ 19.250275, 47.588333 ], [ 19.250417, 47.588404 ], [ 19.250565, 47.588455 ], [ 19.250713, 47.588474 ], [ 19.250897, 47.58847 ], [ 19.251074, 47.588441 ], [ 19.251288, 47.58838 ], [ 19.25153, 47.588276 ], [ 19.251702, 47.588196 ], [ 19.251903, 47.588097 ], [ 19.252116, 47.587984 ], [ 19.252383, 47.587796 ], [ 19.253519, 47.586966 ], [ 19.253572, 47.586886 ], [ 19.25359, 47.586824 ], [ 19.253584, 47.586758 ], [ 19.253525, 47.586702 ], [ 19.25343, 47.586641 ], [ 19.253353, 47.586589 ], [ 19.253324, 47.586542 ], [ 19.25333, 47.586457 ], [ 19.253359, 47.586363 ], [ 19.253773, 47.585311 ], [ 19.253862, 47.58517 ], [ 19.253927, 47.585113 ], [ 19.253992, 47.585062 ], [ 19.254081, 47.585014 ], [ 19.254188, 47.584991 ], [ 19.254389, 47.584963 ], [ 19.255247, 47.584878 ], [ 19.255425, 47.584831 ], [ 19.255537, 47.584779 ], [ 19.255632, 47.584727 ], [ 19.255715, 47.584637 ], [ 19.255744, 47.584557 ], [ 19.25578, 47.584463 ], [ 19.25578, 47.584359 ], [ 19.25575, 47.584246 ], [ 19.255537, 47.583233 ], [ 19.255531, 47.583053 ], [ 19.255549, 47.58294 ], [ 19.255596, 47.582823 ], [ 19.255662, 47.582709 ], [ 19.256502, 47.581804 ], [ 19.256585, 47.581668 ], [ 19.256603, 47.581573 ], [ 19.256609, 47.581484 ], [ 19.256591, 47.58139 ], [ 19.256561, 47.581295 ], [ 19.256502, 47.58121 ], [ 19.256407, 47.581126 ], [ 19.254963, 47.580032 ], [ 19.253679, 47.579028 ], [ 19.253241, 47.578698 ], [ 19.253134, 47.578656 ], [ 19.253034, 47.578646 ], [ 19.252939, 47.578656 ], [ 19.252856, 47.578689 ], [ 19.252773, 47.57874 ], [ 19.251897, 47.57941 ], [ 19.251755, 47.579504 ], [ 19.251086, 47.579872 ], [ 19.250968, 47.579914 ], [ 19.25085, 47.579947 ], [ 19.250707, 47.579961 ], [ 19.250583, 47.579952 ], [ 19.250417, 47.5799 ], [ 19.250317, 47.579825 ], [ 19.250246, 47.579744 ], [ 19.250204, 47.579674 ], [ 19.250193, 47.579579 ], [ 19.25021, 47.579485 ], [ 19.250269, 47.579396 ], [ 19.250346, 47.579325 ], [ 19.251666, 47.578608 ], [ 19.251909, 47.578458 ], [ 19.25198, 47.578377 ], [ 19.252033, 47.578264 ], [ 19.252057, 47.578132 ], [ 19.252039, 47.57801 ], [ 19.25198, 47.577906 ], [ 19.251909, 47.577807 ], [ 19.251797, 47.577722 ], [ 19.251613, 47.577642 ], [ 19.251424, 47.57759 ], [ 19.251217, 47.577571 ], [ 19.250998, 47.57759 ], [ 19.250802, 47.577642 ], [ 19.250619, 47.577727 ], [ 19.245888, 47.58026 ] ] } } ] } 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 calculate_aspect_ratio_rotation(coordinates, preferred_ratio=1.618): # Golden ratio by default """Find rotation that gives closest match to desired aspect ratio.""" best_rotation = 0 best_ratio_diff = float('inf') for angle in range(0, 180, 5): # Check every 5 degrees rad_angle = np.radians(angle) rotated = rotate(coordinates, angle=rad_angle) # Calculate bounding box x_coords = [p[0] for p in rotated] y_coords = [p[1] for p in rotated] width = max(x_coords) - min(x_coords) height = max(y_coords) - min(y_coords) current_ratio = width / height ratio_diff = abs(current_ratio - preferred_ratio) if ratio_diff < best_ratio_diff: best_ratio_diff = ratio_diff best_rotation = angle return best_rotation def calculate_minimal_area_rotation(coordinates): """Find rotation that minimizes the bounding box area.""" best_rotation = 0 min_area = float('inf') for angle in range(0, 180, 5): rad_angle = np.radians(angle) rotated = rotate(coordinates, angle=rad_angle) x_coords = [p[0] for p in rotated] y_coords = [p[1] for p in rotated] width = max(x_coords) - min(x_coords) height = max(y_coords) - min(y_coords) area = width * height if area < min_area: min_area = area best_rotation = angle return best_rotation def calculate_pca_rotation(coordinates): """Use PCA to align the track with its principal axes.""" from sklearn.decomposition import PCA # Convert coordinates to numpy array if not already coords_array = np.array(coordinates) # Fit PCA pca = PCA(n_components=2) pca.fit(coords_array) # Calculate rotation angle from first principal component first_component = pca.components_[0] angle = np.arctan2(first_component[1], first_component[0]) return np.degrees(angle) def calculate_start_straight_rotation(coordinates, straight_length=10): """Align the track so the start/finish straight is vertical/horizontal.""" # Assuming first points are from start/finish straight start_points = coordinates[:straight_length] # Calculate direction vector of the straight dx = start_points[-1][0] - start_points[0][0] dy = start_points[-1][1] - start_points[0][1] # Calculate angle to horizontal angle = np.degrees(np.arctan2(dy, dx)) # Return rotation needed to align with horizontal (0°) or vertical (90°) horizontal_rotation = -angle vertical_rotation = 90 - angle # Return whichever requires less rotation return horizontal_rotation if abs(horizontal_rotation) < abs(vertical_rotation) else vertical_rotation def find_longest_straight(coordinates, window_size=5): """Find the longest approximately straight section of the track, including wrap-around.""" max_distance = 0 best_start_idx = 0 n = len(coordinates) # Helper function to check straightness def is_straight(points, start, end, length): direction = (end - start) / length distances = [] for point in points: projection = start + np.dot(point - start, direction) * direction distance = np.linalg.norm(point - projection) distances.append(distance) return max(distances) < length * 0.05 # 5% tolerance # Check all possible segments, including wrap-around for i in range(n): # Get window_size points, handling wrap-around segment = [] for j in range(window_size): idx = (i + j) % n segment.append(np.array(coordinates[idx])) start = np.array(segment[0]) end = np.array(segment[-1]) length = np.linalg.norm(end - start) if length > max_distance: # Check if all points are roughly on the line if is_straight(segment, start, end, length): max_distance = length best_start_idx = i # Return indices that might wrap around end_idx = (best_start_idx + window_size) % len(coordinates) return best_start_idx, end_idx def calculate_longest_straight_rotation(coordinates): """Find the rotation that places the longest straight section horizontally at the bottom.""" # First find the longest straight section start_idx, end_idx = find_longest_straight(coordinates) best_angle = 0 min_y_diff = float('inf') coordinates_array = np.array(coordinates) # Create a figure for visualization plt.figure(figsize=(15, 5)) # Try angles in smaller increments for more precision for angle in np.linspace(0, 2*np.pi, 72): # 5-degree increments # Rotate the entire track rotated_track = rotate(coordinates_array, angle=angle) # Get y-coordinates of the straight section straight_y1 = rotated_track[start_idx][1] straight_y2 = rotated_track[end_idx][1] y_diff = abs(straight_y1 - straight_y2) # If this is the best rotation so far, show it if y_diff < min_y_diff: min_y_diff = y_diff best_angle = angle # Clear previous plots plt.clf() # Create three subplots plt.subplot(131) plt.title('Original Track') plt.plot(coordinates_array[:, 0], coordinates_array[:, 1], 'k-') plt.plot([coordinates_array[start_idx][0], coordinates_array[end_idx][0]], [coordinates_array[start_idx][1], coordinates_array[end_idx][1]], 'r-', linewidth=2) plt.axis('equal') plt.subplot(132) plt.title(f'Current Rotation ({angle:.1f} rad)') plt.plot(rotated_track[:, 0], rotated_track[:, 1], 'k-') plt.plot([rotated_track[start_idx][0], rotated_track[end_idx][0]], [rotated_track[start_idx][1], rotated_track[end_idx][1]], 'r-', linewidth=2) plt.axis('equal') # Show y-difference plt.text(0.5, -0.1, f'Y-diff: {y_diff:.2f}', horizontalalignment='center', transform=plt.gca().transAxes) plt.subplot(133) plt.title('Y-coordinates of Straight') plt.plot([0, 1], [straight_y1, straight_y2], 'b-') plt.axhline(y=0, color='k', linestyle='--') plt.ylim(min(straight_y1, straight_y2) - 1, max(straight_y1, straight_y2) + 1) plt.tight_layout() plt.pause(0.1) # Show the plot for a moment # Now rotate all coordinates with best angle rotated_coords = rotate(coordinates_array, angle=best_angle) # Check if we need to flip 180 degrees straight_y = np.mean([rotated_coords[start_idx][1], rotated_coords[end_idx][1]]) track_center_y = np.mean(rotated_coords[:, 1]) if straight_y > track_center_y: best_angle += np.pi rotated_coords = rotate(coordinates_array, angle=best_angle) # Show final result plt.clf() plt.title('Final Result') plt.plot(rotated_coords[:, 0], rotated_coords[:, 1], 'k-') plt.plot([rotated_coords[start_idx][0], rotated_coords[end_idx][0]], [rotated_coords[start_idx][1], rotated_coords[end_idx][1]], 'r-', linewidth=2) plt.axis('equal') plt.show() return best_angle def evaluate_rotation_strategies(coordinates): """Compare different rotation strategies and score them.""" strategies = { 'aspect_ratio': calculate_aspect_ratio_rotation, 'minimal_area': calculate_minimal_area_rotation, 'pca': calculate_pca_rotation, 'start_straight': calculate_start_straight_rotation, 'longest_straight': calculate_longest_straight_rotation, } results = {} for name, strategy in strategies.items(): angle = strategy(coordinates) rotated = rotate(coordinates, angle=np.radians(angle)) # Calculate metrics x_coords = [p[0] for p in rotated] y_coords = [p[1] for p in rotated] width = max(x_coords) - min(x_coords) height = max(y_coords) - min(y_coords) results[name] = { 'rotation_angle': angle, 'aspect_ratio': width / height, 'area': width * height, 'width': width, 'height': height } return results def plot_rotation_outcomes(coordinates): # Set up the figure fig = plt.figure(figsize=(15, 12)) grid = plt.GridSpec(3, 2, figure=fig) axes = [ fig.add_subplot(grid[0, 0]), fig.add_subplot(grid[0, 1]), fig.add_subplot(grid[1, 0]), fig.add_subplot(grid[1, 1]), fig.add_subplot(grid[2, :]) ] # Get results from all strategies results = evaluate_rotation_strategies(coordinates) longest_straight_angle = calculate_longest_straight_rotation(coordinates) rotated = rotate(coordinates, angle=np.radians(longest_straight_angle)) x_coords = [p[0] for p in rotated] y_coords = [p[1] for p in rotated] width = max(x_coords) - min(x_coords) height = max(y_coords) - min(y_coords) results['longest_straight'] = { 'rotation_angle': longest_straight_angle, 'aspect_ratio': width / height, 'area': width * height, 'width': width, 'height': height } # Find the longest straight section once start_idx, end_idx = find_longest_straight(coordinates) # Create the straight section handling wrap-around straight_section = [] i = start_idx while i != end_idx: straight_section.append(coordinates[i]) i = (i + 1) % len(coordinates) straight_section.append(coordinates[end_idx]) for (name, metrics), ax in zip(results.items(), axes): # Rotate coordinates using the calculated angle rotated = rotate(coordinates, angle=np.radians(metrics['rotation_angle'])) rotated_straight = rotate(straight_section, angle=np.radians(metrics['rotation_angle'])) # Plot the main track in blue x_coords = [p[0] for p in rotated] y_coords = [p[1] for p in rotated] ax.plot(x_coords, y_coords, 'b-', linewidth=2, label='Track') # Plot the longest straight section in red straight_x = [p[0] for p in rotated_straight] straight_y = [p[1] for p in rotated_straight] ax.plot(straight_x, straight_y, 'r-', linewidth=3, alpha=0.8, label='Longest straight') ax.set_aspect('equal') # Add title with metrics title = f"{name}\nRotation: {metrics['rotation_angle']:.1f}°\n" title += f"Aspect Ratio: {metrics['aspect_ratio']:.2f}\n" title += f"Area: {metrics['area']:.0f}" ax.set_title(title) # Add bounding box min_x, max_x = min(x_coords), max(x_coords) min_y, max_y = min(y_coords), max(y_coords) bbox = plt.Rectangle((min_x, min_y), max_x - min_x, max_y - min_y, fill=False, color='red', linestyle='--') ax.add_patch(bbox) # Add legend ax.legend(loc='upper right') plt.tight_layout() plt.show() def get_best_rotation(coordinates, preferences): """ Get best rotation based on user preferences. preferences: dict with weights for different factors: { 'aspect_ratio_weight': 0.3, 'area_weight': 0.2, 'start_straight_weight': 0.3, 'preferred_orientation': 'landscape', # or 'portrait' 'preferred_ratio': 1.618 # desired aspect ratio } """ results = evaluate_rotation_strategies(coordinates) scores = {} for strategy, metrics in results.items(): score = 0 # Aspect ratio scoring if preferences['preferred_orientation'] == 'landscape': score += (metrics['aspect_ratio'] > 1) * preferences['aspect_ratio_weight'] else: score += (metrics['aspect_ratio'] < 1) * preferences['aspect_ratio_weight'] # Area efficiency scoring min_area = min(r['area'] for r in results.values()) score += (min_area / metrics['area']) * preferences['area_weight'] # Start straight alignment scoring if strategy == 'start_straight': score += preferences['start_straight_weight'] scores[strategy] = score best_strategy = max(scores.items(), key=lambda x: x[1])[0] return results[best_strategy]['rotation_angle'] coordinates = data.get("features", [])[0].get("geometry", {}).get("coordinates", []) plot_rotation_outcomes(coordinates) # preferences = { # 'aspect_ratio_weight': 0.3, # 'area_weight': 0.2, # 'start_straight_weight': 0.3, # 'preferred_orientation': 'landscape', # 'preferred_ratio': 1.618 # } # optimal_rotation = get_best_rotation(coordinates, preferences)