|
- 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)
|