1628 lines
66 KiB
Python
1628 lines
66 KiB
Python
"""
|
|
3D Video Animation Generator
|
|
Creates professional Google Earth-style 3D video animations from GPS route data
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import math
|
|
import requests
|
|
import cv2
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
import tempfile
|
|
import shutil
|
|
from datetime import datetime
|
|
import random
|
|
|
|
def generate_3d_video_animation(project_name, resources_folder, label_widget, progress_widget, popup_widget, clock_module):
|
|
"""
|
|
Generate a 3D video animation similar to Relive
|
|
|
|
Args:
|
|
project_name: Name of the project
|
|
resources_folder: Path to resources folder
|
|
label_widget: Kivy label for status updates
|
|
progress_widget: Kivy progress bar
|
|
popup_widget: Kivy popup to dismiss when done
|
|
clock_module: Kivy Clock module for scheduling
|
|
"""
|
|
|
|
def update_progress(progress_val, status_text):
|
|
"""Update UI from background thread"""
|
|
def _update(dt):
|
|
progress_widget.value = progress_val
|
|
label_widget.text = status_text
|
|
clock_module.schedule_once(_update, 0)
|
|
|
|
def finish_generation(success, message, output_path=None):
|
|
"""Finish the generation process"""
|
|
def _finish(dt):
|
|
if popup_widget:
|
|
popup_widget.dismiss()
|
|
|
|
# Show result popup
|
|
from kivy.uix.popup import Popup
|
|
from kivy.uix.boxlayout import BoxLayout
|
|
from kivy.uix.button import Button
|
|
from kivy.uix.label import Label
|
|
|
|
result_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
|
|
|
if success:
|
|
result_label = Label(
|
|
text=f"3D Video Generated Successfully!\n\nSaved to:\n{output_path}",
|
|
color=(0, 1, 0, 1),
|
|
halign="center"
|
|
)
|
|
open_btn = Button(
|
|
text="Open Video Folder",
|
|
size_hint_y=None,
|
|
height=40,
|
|
background_color=(0.2, 0.7, 0.2, 1)
|
|
)
|
|
open_btn.bind(on_press=lambda x: (os.system(f"xdg-open '{os.path.dirname(output_path)}'"), result_popup.dismiss()))
|
|
result_layout.add_widget(result_label)
|
|
result_layout.add_widget(open_btn)
|
|
else:
|
|
result_label = Label(
|
|
text=f"Generation Failed:\n{message}",
|
|
color=(1, 0, 0, 1),
|
|
halign="center"
|
|
)
|
|
result_layout.add_widget(result_label)
|
|
|
|
close_btn = Button(
|
|
text="Close",
|
|
size_hint_y=None,
|
|
height=40,
|
|
background_color=(0.3, 0.3, 0.3, 1)
|
|
)
|
|
|
|
result_layout.add_widget(close_btn)
|
|
|
|
result_popup = Popup(
|
|
title="3D Video Generation Result",
|
|
content=result_layout,
|
|
size_hint=(0.9, 0.6),
|
|
auto_dismiss=False
|
|
)
|
|
|
|
close_btn.bind(on_press=lambda x: result_popup.dismiss())
|
|
result_popup.open()
|
|
|
|
clock_module.schedule_once(_finish, 0)
|
|
|
|
def run_generation():
|
|
"""Main generation function"""
|
|
try:
|
|
# Step 1: Load route data
|
|
update_progress(10, "Loading route data...")
|
|
project_folder = os.path.join(resources_folder, "projects", project_name)
|
|
positions_path = os.path.join(project_folder, "positions.json")
|
|
|
|
if not os.path.exists(positions_path):
|
|
finish_generation(False, "No route data found!")
|
|
return
|
|
|
|
with open(positions_path, "r") as f:
|
|
positions = json.load(f)
|
|
|
|
if len(positions) < 10:
|
|
finish_generation(False, "Route too short for 3D animation (minimum 10 points)")
|
|
return
|
|
|
|
# Step 2: Calculate route bounds and center
|
|
update_progress(20, "Calculating route boundaries...")
|
|
lats = [pos['latitude'] for pos in positions]
|
|
lons = [pos['longitude'] for pos in positions]
|
|
|
|
center_lat = sum(lats) / len(lats)
|
|
center_lon = sum(lons) / len(lons)
|
|
|
|
min_lat, max_lat = min(lats), max(lats)
|
|
min_lon, max_lon = min(lons), max(lons)
|
|
|
|
# Step 3: Generate frames with space entry sequence
|
|
update_progress(30, "Generating 3D frames with space entry...")
|
|
|
|
# Create temporary directory for frames
|
|
temp_dir = tempfile.mkdtemp()
|
|
frames_dir = os.path.join(temp_dir, "frames")
|
|
os.makedirs(frames_dir)
|
|
|
|
# Video settings
|
|
width, height = 1920, 1080
|
|
fps = 30
|
|
entry_frames = 90 # 3 seconds at 30fps for space entry
|
|
total_frames = entry_frames + len(positions) * 2 # Entry + route animation
|
|
|
|
frame_counter = 0
|
|
|
|
# Generate space entry sequence (3 seconds)
|
|
update_progress(30, "Creating space entry sequence...")
|
|
for i in range(entry_frames):
|
|
progress = 30 + (i / total_frames) * 40
|
|
update_progress(progress, f"Space entry frame {i+1}/{entry_frames}...")
|
|
|
|
try:
|
|
frame = create_space_entry_frame(
|
|
positions[0], center_lat, center_lon,
|
|
min_lat, max_lat, min_lon, max_lon,
|
|
width, height, i, entry_frames
|
|
)
|
|
|
|
frame_path = os.path.join(frames_dir, f"frame_{frame_counter:06d}.png")
|
|
cv2.imwrite(frame_path, frame)
|
|
frame_counter += 1
|
|
|
|
except Exception as e:
|
|
print(f"Error generating space entry frame {i}: {e}")
|
|
# Create a simple fallback frame
|
|
fallback_frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
fallback_frame[:] = (0, 0, 50) # Space-like background
|
|
frame_path = os.path.join(frames_dir, f"frame_{frame_counter:06d}.png")
|
|
cv2.imwrite(frame_path, fallback_frame)
|
|
frame_counter += 1
|
|
|
|
# Generate route following frames
|
|
for i, pos in enumerate(positions):
|
|
progress = 30 + ((entry_frames + i) / total_frames) * 40
|
|
update_progress(progress, f"Route frame {i+1}/{len(positions)}...")
|
|
|
|
try:
|
|
frame = create_3d_frame(
|
|
pos, positions, i, center_lat, center_lon,
|
|
min_lat, max_lat, min_lon, max_lon,
|
|
width, height
|
|
)
|
|
|
|
# Save frame
|
|
frame_path = os.path.join(frames_dir, f"frame_{frame_counter:06d}.png")
|
|
cv2.imwrite(frame_path, frame)
|
|
frame_counter += 1
|
|
|
|
except Exception as e:
|
|
print(f"Error generating route frame {i}: {e}")
|
|
# Create a simple fallback frame to continue generation
|
|
fallback_frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
fallback_frame[:] = (50, 50, 100) # Dark blue background
|
|
frame_path = os.path.join(frames_dir, f"frame_{frame_counter:06d}.png")
|
|
cv2.imwrite(frame_path, fallback_frame)
|
|
frame_counter += 1
|
|
|
|
# Add transition bridge frame (smooth transition from space to route)
|
|
try:
|
|
update_progress(progress, "Creating transition bridge...")
|
|
transition_frame = create_transition_bridge_frame(
|
|
positions[0], center_lat, center_lon,
|
|
min_lat, max_lat, min_lon, max_lon,
|
|
width, height
|
|
)
|
|
frame_path = os.path.join(frames_dir, f"frame_{frame_counter:06d}.png")
|
|
cv2.imwrite(frame_path, transition_frame)
|
|
frame_counter += 1
|
|
except Exception as e:
|
|
print(f"Warning: Could not create transition bridge frame: {e}")
|
|
|
|
# Step 4: Create video
|
|
update_progress(75, "Compiling video...")
|
|
|
|
# Output path
|
|
output_filename = f"{project_name}_3d_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
|
|
output_path = os.path.join(project_folder, output_filename)
|
|
|
|
# Create video writer
|
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
|
|
|
# Add frames to video
|
|
frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
|
|
for frame_file in frame_files:
|
|
frame_path = os.path.join(frames_dir, frame_file)
|
|
frame = cv2.imread(frame_path)
|
|
video_writer.write(frame)
|
|
|
|
video_writer.release()
|
|
|
|
# Step 5: Add audio (optional)
|
|
update_progress(90, "Adding finishing touches...")
|
|
|
|
# Clean up
|
|
shutil.rmtree(temp_dir)
|
|
|
|
update_progress(100, "3D Video generated successfully!")
|
|
finish_generation(True, "Success!", output_path)
|
|
|
|
except Exception as e:
|
|
finish_generation(False, str(e))
|
|
|
|
# Start generation in background
|
|
import threading
|
|
thread = threading.Thread(target=run_generation)
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
def create_3d_frame(current_pos, all_positions, frame_index, center_lat, center_lon,
|
|
min_lat, max_lat, min_lon, max_lon, width, height):
|
|
"""
|
|
Create a Google Earth-style 3D frame with camera following the route
|
|
"""
|
|
# Create canvas
|
|
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
|
|
# Enhanced camera following system
|
|
camera_pos, camera_target, camera_bearing = calculate_dynamic_camera_position(
|
|
current_pos, all_positions, frame_index, min_lat, max_lat, min_lon, max_lon
|
|
)
|
|
|
|
# Google Earth-style perspective parameters with improved aerial view
|
|
base_camera_height = 1500 + 1000 * math.sin(frame_index * 0.02) # 1000-3000m range
|
|
camera_height = base_camera_height + 500 * math.sin(frame_index * 0.05) # Add variation
|
|
view_distance = 3000 # Increased view distance for better aerial perspective
|
|
tilt_angle = 65 + 8 * math.sin(frame_index * 0.03) # Dynamic tilt for cinematic effect
|
|
fov = 75 # Slightly wider field of view for aerial shots
|
|
|
|
# Create enhanced terrain background
|
|
create_terrain_background(frame, width, height, camera_pos['latitude'], camera_pos['longitude'], camera_bearing, tilt_angle)
|
|
|
|
# Transform all route points to 3D camera space
|
|
route_points_3d = []
|
|
for i, pos in enumerate(all_positions):
|
|
# Calculate distance from camera
|
|
dist_to_camera = calculate_distance(camera_pos['latitude'], camera_pos['longitude'],
|
|
pos['latitude'], pos['longitude'])
|
|
|
|
if dist_to_camera > view_distance * 2: # Skip points too far away
|
|
continue
|
|
|
|
# Get elevation for this point
|
|
elevation = get_simulated_elevation(pos['latitude'], pos['longitude'], i)
|
|
|
|
# Convert to 3D screen coordinates
|
|
screen_x, screen_y, is_visible = world_to_screen_3d(
|
|
pos['latitude'], pos['longitude'], elevation,
|
|
camera_pos['latitude'], camera_pos['longitude'], camera_height,
|
|
camera_bearing, tilt_angle, width, height, view_distance
|
|
)
|
|
|
|
if is_visible:
|
|
# Mark points as past, current, or future
|
|
# Ensure at least the current position (frame_index) is marked as past
|
|
is_past_or_current = i <= frame_index
|
|
route_points_3d.append((screen_x, screen_y, is_past_or_current))
|
|
|
|
# Draw route with enhanced 3D effects
|
|
draw_3d_route(frame, route_points_3d, frame_index)
|
|
|
|
# Add Google Earth-style UI overlays
|
|
add_google_earth_ui(frame, current_pos, camera_bearing, width, height, frame_index, len(all_positions))
|
|
|
|
# Add atmospheric effects
|
|
add_atmospheric_perspective(frame, width, height)
|
|
|
|
return frame
|
|
|
|
def calculate_bearing(lat1, lon1, lat2, lon2):
|
|
"""Calculate bearing between two GPS points"""
|
|
lat1_rad = math.radians(lat1)
|
|
lat2_rad = math.radians(lat2)
|
|
dlon_rad = math.radians(lon2 - lon1)
|
|
|
|
y = math.sin(dlon_rad) * math.cos(lat2_rad)
|
|
x = math.cos(lat1_rad) * math.sin(lat2_rad) - math.sin(lat1_rad) * math.cos(lat2_rad) * math.cos(dlon_rad)
|
|
|
|
bearing = math.atan2(y, x)
|
|
bearing = math.degrees(bearing)
|
|
bearing = (bearing + 360) % 360
|
|
|
|
return bearing
|
|
|
|
def create_terrain_background(frame, width, height, camera_lat, camera_lon, bearing, tilt_angle):
|
|
"""Create a professional Google Earth-style terrain background"""
|
|
|
|
# Enhanced sky gradient with realistic atmospheric scattering
|
|
for y in range(int(height * 0.35)): # Sky takes upper 35%
|
|
sky_intensity = y / (height * 0.35)
|
|
|
|
# Realistic sky colors with atmospheric perspective
|
|
horizon_r, horizon_g, horizon_b = 255, 248, 220 # Warm horizon
|
|
zenith_r, zenith_g, zenith_b = 135, 206, 235 # Sky blue
|
|
|
|
r = int(horizon_r + (zenith_r - horizon_r) * sky_intensity)
|
|
g = int(horizon_g + (zenith_g - horizon_g) * sky_intensity)
|
|
b = int(horizon_b + (zenith_b - horizon_b) * sky_intensity)
|
|
|
|
frame[y, :] = (b, g, r) # BGR format for OpenCV
|
|
|
|
# Realistic terrain with multiple layers and textures
|
|
terrain_start_y = int(height * 0.35)
|
|
create_enhanced_terrain_layer(frame, width, height, terrain_start_y, camera_lat, camera_lon)
|
|
|
|
# Add atmospheric haze for depth
|
|
add_atmospheric_haze(frame, width, height, terrain_start_y)
|
|
|
|
# Add realistic cloud shadows
|
|
add_cloud_shadows(frame, width, height, terrain_start_y)
|
|
|
|
def create_enhanced_terrain_layer(frame, width, height, start_y, camera_lat, camera_lon):
|
|
"""Create enhanced terrain with realistic colors and textures"""
|
|
|
|
for y in range(start_y, height):
|
|
distance_factor = (y - start_y) / (height - start_y)
|
|
|
|
for x in range(width):
|
|
# Multiple noise layers for realistic terrain variation
|
|
terrain_color = generate_enhanced_terrain_color(x, y, camera_lat, camera_lon, width, height, distance_factor)
|
|
frame[y, x] = terrain_color
|
|
|
|
def generate_enhanced_terrain_color(x, y, camera_lat, camera_lon, width, height, distance_factor):
|
|
"""Generate enhanced terrain color with realistic geographic features"""
|
|
|
|
# Base terrain using multiple octaves of noise
|
|
noise_scale1 = 0.01
|
|
noise_scale2 = 0.005
|
|
noise_scale3 = 0.002
|
|
|
|
# Primary terrain features
|
|
terrain1 = math.sin(x * noise_scale1) * math.sin(y * noise_scale1)
|
|
terrain2 = math.sin(x * noise_scale2 + 100) * math.sin(y * noise_scale2 + 100) * 0.7
|
|
terrain3 = math.sin(x * noise_scale3 + 200) * math.sin(y * noise_scale3 + 200) * 0.3
|
|
|
|
combined_terrain = terrain1 + terrain2 + terrain3
|
|
|
|
# Simulate geographic coordinate influence
|
|
lat_influence = math.sin(camera_lat * 0.1) * 0.5
|
|
lon_influence = math.cos(camera_lon * 0.1) * 0.3
|
|
geographic_factor = lat_influence + lon_influence
|
|
|
|
final_terrain = combined_terrain + geographic_factor
|
|
|
|
# Classify terrain types based on noise
|
|
if final_terrain > 1.2:
|
|
# High mountains - snow-capped peaks
|
|
base_color = (240, 248, 255) # Alice blue
|
|
elif final_terrain > 0.8:
|
|
# Mountains - rocky gray/brown
|
|
base_color = (105, 105, 105) # Dim gray
|
|
elif final_terrain > 0.4:
|
|
# Hills - forest green
|
|
base_color = (34, 139, 34) # Forest green
|
|
elif final_terrain > 0.1:
|
|
# Plains - grassland
|
|
base_color = (124, 252, 0) # Lawn green
|
|
elif final_terrain > -0.2:
|
|
# Agricultural areas - golden
|
|
base_color = (255, 215, 0) # Gold
|
|
elif final_terrain > -0.5:
|
|
# Desert/arid - sandy brown
|
|
base_color = (244, 164, 96) # Sandy brown
|
|
else:
|
|
# Water bodies - deep blue
|
|
base_color = (25, 25, 112) # Midnight blue
|
|
|
|
# Apply distance-based atmospheric perspective
|
|
atmosphere_fade = 1.0 - (distance_factor * 0.4)
|
|
final_color = tuple(int(c * atmosphere_fade + 200 * (1 - atmosphere_fade)) for c in base_color)
|
|
|
|
# Add subtle texture variation
|
|
texture_noise = (math.sin(x * 0.1) * math.sin(y * 0.1)) * 10
|
|
final_color = tuple(max(0, min(255, c + int(texture_noise))) for c in final_color)
|
|
|
|
return final_color
|
|
|
|
def add_atmospheric_haze(frame, width, height, terrain_start_y):
|
|
"""Add realistic atmospheric haze for depth perception"""
|
|
|
|
haze_overlay = np.zeros_like(frame)
|
|
|
|
for y in range(terrain_start_y, height):
|
|
distance_factor = (y - terrain_start_y) / (height - terrain_start_y)
|
|
haze_intensity = distance_factor * 0.3 # Stronger haze in distance
|
|
|
|
if haze_intensity > 0:
|
|
haze_color = int(220 * haze_intensity) # Light blue-gray haze
|
|
haze_overlay[y, :] = (haze_color, haze_color, haze_color)
|
|
|
|
# Blend haze with terrain
|
|
cv2.addWeighted(frame, 1.0, haze_overlay, 0.3, 0, frame)
|
|
|
|
def add_cloud_shadows(frame, width, height, terrain_start_y):
|
|
"""Add realistic cloud shadows on terrain"""
|
|
|
|
shadow_overlay = np.zeros_like(frame)
|
|
|
|
# Generate cloud shadow patterns
|
|
for shadow_id in range(3):
|
|
shadow_center_x = int(width * (0.2 + shadow_id * 0.3))
|
|
shadow_center_y = int(terrain_start_y + (height - terrain_start_y) * 0.3)
|
|
|
|
shadow_radius = 80 + shadow_id * 30
|
|
|
|
# Create soft circular shadows
|
|
for y in range(max(terrain_start_y, shadow_center_y - shadow_radius),
|
|
min(height, shadow_center_y + shadow_radius)):
|
|
for x in range(max(0, shadow_center_x - shadow_radius),
|
|
min(width, shadow_center_x + shadow_radius)):
|
|
|
|
distance = math.sqrt((x - shadow_center_x)**2 + (y - shadow_center_y)**2)
|
|
|
|
if distance < shadow_radius:
|
|
shadow_intensity = 1.0 - (distance / shadow_radius)
|
|
shadow_intensity *= 0.3 # Subtle shadows
|
|
|
|
shadow_value = int(50 * shadow_intensity)
|
|
shadow_overlay[y, x] = (shadow_value, shadow_value, shadow_value)
|
|
|
|
# Apply shadows
|
|
frame_dark = frame.astype(np.int32) - shadow_overlay.astype(np.int32)
|
|
frame[:] = np.clip(frame_dark, 0, 255).astype(np.uint8)
|
|
|
|
def calculate_visible_bounds(camera_lat, camera_lon, bearing, view_distance, width, height):
|
|
"""Calculate the bounds of the visible area"""
|
|
# This is a simplified calculation for the demo
|
|
# In a real implementation, you'd use proper 3D projection math
|
|
lat_offset = view_distance / 111000 # Rough conversion to degrees
|
|
lon_offset = view_distance / (111000 * math.cos(math.radians(camera_lat)))
|
|
|
|
return {
|
|
'min_lat': camera_lat - lat_offset,
|
|
'max_lat': camera_lat + lat_offset,
|
|
'min_lon': camera_lon - lon_offset,
|
|
'max_lon': camera_lon + lon_offset
|
|
}
|
|
|
|
def world_to_screen_3d(world_lat, world_lon, elevation, camera_lat, camera_lon, camera_height,
|
|
bearing, tilt_angle, screen_width, screen_height, view_distance):
|
|
"""Transform world coordinates to 3D screen coordinates"""
|
|
# Calculate relative position
|
|
lat_diff = world_lat - camera_lat
|
|
lon_diff = world_lon - camera_lon
|
|
|
|
# Convert to meters (approximate)
|
|
x_meters = lon_diff * 111000 * math.cos(math.radians(camera_lat))
|
|
y_meters = lat_diff * 111000
|
|
z_meters = elevation - camera_height
|
|
|
|
# Rotate based on bearing
|
|
bearing_rad = math.radians(-bearing) # Negative for correct rotation
|
|
rotated_x = x_meters * math.cos(bearing_rad) - y_meters * math.sin(bearing_rad)
|
|
rotated_y = x_meters * math.sin(bearing_rad) + y_meters * math.cos(bearing_rad)
|
|
|
|
# Check if point is in front of camera
|
|
if rotated_y < 0:
|
|
return 0, 0, False
|
|
|
|
# Apply perspective projection
|
|
perspective_scale = view_distance / max(rotated_y, 1)
|
|
|
|
# Convert to screen coordinates
|
|
screen_x = int(screen_width / 2 + rotated_x * perspective_scale * 0.5)
|
|
|
|
# Apply tilt for vertical positioning
|
|
tilt_factor = math.sin(math.radians(tilt_angle))
|
|
horizon_y = screen_height * 0.4 # Horizon line
|
|
screen_y = int(horizon_y + (z_meters * perspective_scale * tilt_factor * 0.1) +
|
|
(rotated_y * perspective_scale * 0.2))
|
|
|
|
# Check if point is visible on screen
|
|
is_visible = (0 <= screen_x < screen_width and 0 <= screen_y < screen_height)
|
|
|
|
return screen_x, screen_y, is_visible
|
|
|
|
def get_simulated_elevation(lat, lon, frame_index):
|
|
"""Generate simulated elevation data"""
|
|
# Create varied terrain using sine waves
|
|
elevation = (
|
|
50 * math.sin(lat * 100) +
|
|
30 * math.sin(lon * 80) +
|
|
20 * math.sin((lat + lon) * 60) +
|
|
10 * math.sin(frame_index * 0.1) # Dynamic element
|
|
)
|
|
return max(0, elevation) # Ensure non-negative elevation
|
|
|
|
def draw_3d_route(frame, route_points_3d, current_frame_index):
|
|
"""Draw the route with 3D perspective effects"""
|
|
if len(route_points_3d) < 2:
|
|
return
|
|
|
|
# Draw route segments
|
|
for i in range(1, len(route_points_3d)):
|
|
x1, y1, is_past1 = route_points_3d[i-1]
|
|
x2, y2, is_past2 = route_points_3d[i]
|
|
|
|
# Color based on position relative to current point
|
|
if is_past1 and is_past2:
|
|
# Past route - blue to cyan gradient
|
|
color = (255, 200, 100) # Cyan-ish
|
|
thickness = 4
|
|
else:
|
|
# Future route - red gradient
|
|
color = (100, 100, 255) # Red-ish
|
|
thickness = 3
|
|
|
|
# Draw line with shadow for depth
|
|
cv2.line(frame, (x1+2, y1+2), (x2+2, y2+2), (50, 50, 50), thickness+2)
|
|
cv2.line(frame, (x1, y1), (x2, y2), color, thickness)
|
|
|
|
# Draw current position marker
|
|
if route_points_3d:
|
|
# Find the current position - look for the last "past" point, or use the first point
|
|
current_x, current_y = None, None
|
|
|
|
# Try to find the last past point
|
|
for x, y, is_past in route_points_3d:
|
|
if is_past:
|
|
current_x, current_y = x, y
|
|
|
|
# If no past points found (beginning of route), use the first point
|
|
if current_x is None and len(route_points_3d) > 0:
|
|
current_x, current_y, _ = route_points_3d[0]
|
|
|
|
# Only draw marker if we have a valid position
|
|
if current_x is not None and current_y is not None:
|
|
# Pulsing current position marker
|
|
pulse_size = int(12 + 8 * math.sin(current_frame_index * 0.3))
|
|
|
|
# Shadow
|
|
cv2.circle(frame, (current_x+3, current_y+3), pulse_size, (0, 0, 0), -1)
|
|
# Outer ring
|
|
cv2.circle(frame, (current_x, current_y), pulse_size, (0, 255, 255), -1)
|
|
# Inner ring
|
|
cv2.circle(frame, (current_x, current_y), pulse_size-4, (255, 255, 255), 2)
|
|
# Center dot
|
|
cv2.circle(frame, (current_x, current_y), 3, (255, 0, 0), -1)
|
|
|
|
def add_google_earth_ui(frame, current_pos, bearing, width, height, frame_index, total_frames):
|
|
"""Add Google Earth-style UI elements"""
|
|
# Speed and info panel (top-left)
|
|
panel_width = 250
|
|
panel_height = 120
|
|
overlay = frame.copy()
|
|
|
|
# Semi-transparent panel
|
|
cv2.rectangle(overlay, (10, 10), (panel_width, panel_height), (50, 50, 50), -1)
|
|
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
|
|
|
|
# Panel border
|
|
cv2.rectangle(frame, (10, 10), (panel_width, panel_height), (200, 200, 200), 2)
|
|
|
|
# Text information
|
|
speed = current_pos.get('speed', 0)
|
|
timestamp = current_pos.get('deviceTime', '')
|
|
|
|
y_pos = 35
|
|
cv2.putText(frame, f"Speed: {speed:.1f} km/h", (20, y_pos),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
|
|
|
|
y_pos += 25
|
|
cv2.putText(frame, f"Bearing: {bearing:.0f}°", (20, y_pos),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
|
|
|
|
y_pos += 25
|
|
if timestamp:
|
|
cv2.putText(frame, f"Time: {timestamp[:16]}", (20, y_pos),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
|
|
|
y_pos += 25
|
|
progress = (frame_index + 1) / total_frames * 100
|
|
cv2.putText(frame, f"Progress: {progress:.1f}%", (20, y_pos),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
|
|
|
# Compass (top-right)
|
|
compass_center_x = width - 80
|
|
compass_center_y = 80
|
|
compass_radius = 40
|
|
|
|
# Compass background
|
|
cv2.circle(frame, (compass_center_x, compass_center_y), compass_radius, (50, 50, 50), -1)
|
|
cv2.circle(frame, (compass_center_x, compass_center_y), compass_radius, (200, 200, 200), 2)
|
|
|
|
# North indicator
|
|
north_x = compass_center_x + int((compass_radius - 10) * math.sin(math.radians(-bearing)))
|
|
north_y = compass_center_y - int((compass_radius - 10) * math.cos(math.radians(-bearing)))
|
|
cv2.arrowedLine(frame, (compass_center_x, compass_center_y), (north_x, north_y), (0, 0, 255), 3)
|
|
|
|
# N label
|
|
cv2.putText(frame, "N", (compass_center_x - 8, compass_center_y - compass_radius - 10),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
|
|
|
# Progress bar (bottom)
|
|
progress_bar_width = width - 40
|
|
progress_bar_height = 10
|
|
progress_bar_x = 20
|
|
progress_bar_y = height - 30
|
|
|
|
# Background
|
|
cv2.rectangle(frame, (progress_bar_x, progress_bar_y),
|
|
(progress_bar_x + progress_bar_width, progress_bar_y + progress_bar_height),
|
|
(100, 100, 100), -1)
|
|
|
|
# Progress fill
|
|
progress_width = int(progress_bar_width * progress / 100)
|
|
cv2.rectangle(frame, (progress_bar_x, progress_bar_y),
|
|
(progress_bar_x + progress_width, progress_bar_y + progress_bar_height),
|
|
(0, 255, 100), -1)
|
|
|
|
# Border
|
|
cv2.rectangle(frame, (progress_bar_x, progress_bar_y),
|
|
(progress_bar_x + progress_bar_width, progress_bar_y + progress_bar_height),
|
|
(200, 200, 200), 1)
|
|
|
|
def add_atmospheric_perspective(frame, width, height):
|
|
"""Add distance fog effect for realism"""
|
|
# Create fog gradient overlay
|
|
fog_overlay = np.zeros_like(frame)
|
|
|
|
# Fog is stronger towards the horizon
|
|
horizon_y = int(height * 0.4)
|
|
for y in range(horizon_y, height):
|
|
fog_intensity = min(0.3, (y - horizon_y) / (height - horizon_y) * 0.3)
|
|
fog_color = int(200 * fog_intensity)
|
|
fog_overlay[y, :] = (fog_color, fog_color, fog_color)
|
|
|
|
# Blend fog with frame
|
|
cv2.addWeighted(frame, 1.0, fog_overlay, 0.5, 0, frame)
|
|
|
|
def get_elevation_data(lat, lon):
|
|
"""
|
|
Get elevation data for a coordinate (optional enhancement)
|
|
"""
|
|
try:
|
|
# Using a free elevation API
|
|
url = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}"
|
|
response = requests.get(url, timeout=5)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data['results'][0]['elevation']
|
|
except Exception:
|
|
pass
|
|
return 0 # Default elevation
|
|
|
|
def calculate_dynamic_camera_position(current_pos, all_positions, frame_index, min_lat, max_lat, min_lon, max_lon):
|
|
"""
|
|
Calculate dynamic camera position that follows the route smoothly
|
|
"""
|
|
camera_lat = current_pos['latitude']
|
|
camera_lon = current_pos['longitude']
|
|
|
|
# Dynamic look-ahead based on speed and terrain
|
|
speed = current_pos.get('speed', 0)
|
|
base_look_ahead = max(3, min(10, int(speed / 10))) # Adjust based on speed
|
|
|
|
# Look ahead in the route for camera direction
|
|
look_ahead_frames = min(base_look_ahead, len(all_positions) - frame_index - 1)
|
|
|
|
if look_ahead_frames > 0:
|
|
target_pos = all_positions[frame_index + look_ahead_frames]
|
|
target_lat = target_pos['latitude']
|
|
target_lon = target_pos['longitude']
|
|
else:
|
|
# Use previous points to maintain direction
|
|
if frame_index > 0:
|
|
prev_pos = all_positions[frame_index - 1]
|
|
# Extrapolate forward
|
|
lat_diff = camera_lat - prev_pos['latitude']
|
|
lon_diff = camera_lon - prev_pos['longitude']
|
|
target_lat = camera_lat + lat_diff
|
|
target_lon = camera_lon + lon_diff
|
|
else:
|
|
target_lat = camera_lat
|
|
target_lon = camera_lon
|
|
|
|
# Calculate smooth bearing with momentum
|
|
bearing = calculate_bearing(camera_lat, camera_lon, target_lat, target_lon)
|
|
|
|
# Add slight camera offset for better viewing angle
|
|
offset_distance = 50 # meters
|
|
offset_angle = bearing + 45 # 45 degrees offset for better perspective
|
|
|
|
# Calculate offset position
|
|
offset_lat = camera_lat + (offset_distance / 111000) * math.cos(math.radians(offset_angle))
|
|
offset_lon = camera_lon + (offset_distance / (111000 * math.cos(math.radians(camera_lat)))) * math.sin(math.radians(offset_angle))
|
|
|
|
camera_pos = {
|
|
'latitude': offset_lat,
|
|
'longitude': offset_lon
|
|
}
|
|
|
|
camera_target = {
|
|
'latitude': target_lat,
|
|
'longitude': target_lon
|
|
}
|
|
|
|
return camera_pos, camera_target, bearing
|
|
|
|
def calculate_distance(lat1, lon1, lat2, lon2):
|
|
"""Calculate distance between two GPS points in meters"""
|
|
# Haversine formula
|
|
R = 6371000 # Earth's radius in meters
|
|
phi1 = math.radians(lat1)
|
|
phi2 = math.radians(lat2)
|
|
delta_phi = math.radians(lat2 - lat1)
|
|
delta_lambda = math.radians(lon2 - lon1)
|
|
|
|
a = math.sin(delta_phi/2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda/2)**2
|
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
|
|
|
return R * c
|
|
|
|
def world_to_camera_screen(world_lat, world_lon, elevation, camera_pos, camera_target, camera_height,
|
|
bearing, tilt_angle, fov, screen_width, screen_height):
|
|
"""
|
|
Advanced 3D transformation from world coordinates to screen coordinates
|
|
"""
|
|
# Convert GPS to local coordinates relative to camera
|
|
lat_diff = world_lat - camera_pos['latitude']
|
|
lon_diff = world_lon - camera_pos['longitude']
|
|
|
|
# Convert to meters (more accurate conversion)
|
|
x_meters = lon_diff * 111320 * math.cos(math.radians(camera_pos['latitude']))
|
|
y_meters = lat_diff * 110540
|
|
z_meters = elevation - camera_height
|
|
|
|
# Apply camera rotation based on bearing
|
|
bearing_rad = math.radians(-bearing)
|
|
tilt_rad = math.radians(tilt_angle)
|
|
|
|
# Rotate around Z axis (bearing)
|
|
rotated_x = x_meters * math.cos(bearing_rad) - y_meters * math.sin(bearing_rad)
|
|
rotated_y = x_meters * math.sin(bearing_rad) + y_meters * math.cos(bearing_rad)
|
|
rotated_z = z_meters
|
|
|
|
# Apply tilt rotation
|
|
final_y = rotated_y * math.cos(tilt_rad) - rotated_z * math.sin(tilt_rad)
|
|
final_z = rotated_y * math.sin(tilt_rad) + rotated_z * math.cos(tilt_rad)
|
|
final_x = rotated_x
|
|
|
|
# Check if point is in front of camera
|
|
if final_y <= 0:
|
|
return 0, 0, float('inf'), False
|
|
|
|
# Perspective projection
|
|
fov_rad = math.radians(fov)
|
|
f = (screen_width / 2) / math.tan(fov_rad / 2) # Focal length
|
|
|
|
# Project to screen
|
|
screen_x = int(screen_width / 2 + (final_x * f) / final_y)
|
|
screen_y = int(screen_height / 2 - (final_z * f) / final_y)
|
|
|
|
# Calculate depth for sorting
|
|
depth = final_y
|
|
|
|
# Check if point is visible on screen
|
|
is_visible = (0 <= screen_x < screen_width and 0 <= screen_y < screen_height)
|
|
|
|
return screen_x, screen_y, depth, is_visible
|
|
|
|
def get_enhanced_elevation(lat, lon, point_index, frame_index):
|
|
"""
|
|
Generate more realistic elevation data with variation
|
|
"""
|
|
# Base elevation using multiple harmonics
|
|
base_elevation = (
|
|
100 * math.sin(lat * 50) +
|
|
70 * math.sin(lon * 40) +
|
|
50 * math.sin((lat + lon) * 30) +
|
|
30 * math.sin(lat * 200) * math.cos(lon * 150) +
|
|
20 * math.sin(point_index * 0.1) # Smooth variation along route
|
|
)
|
|
|
|
# Add temporal variation for dynamic feel
|
|
time_variation = 10 * math.sin(frame_index * 0.05 + point_index * 0.2)
|
|
|
|
# Ensure realistic elevation range
|
|
elevation = max(0, min(500, base_elevation + time_variation))
|
|
|
|
return elevation
|
|
|
|
def create_space_entry_frame(start_pos, center_lat, center_lon, min_lat, max_lat, min_lon, max_lon,
|
|
width, height, frame_index, total_entry_frames):
|
|
"""
|
|
Create a realistic Google Earth-style space entry frame
|
|
"""
|
|
# Create high-resolution canvas
|
|
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
|
|
# Calculate entry progress (0 to 1)
|
|
entry_progress = frame_index / total_entry_frames
|
|
|
|
# Realistic altitude progression
|
|
max_altitude = 50000 # 50km - edge of space
|
|
min_altitude = 2000 # 2km - final descent altitude
|
|
|
|
# Smooth descent with realistic deceleration
|
|
altitude_progress = 1 - (1 - entry_progress) ** 2.5
|
|
current_altitude = max_altitude - (max_altitude - min_altitude) * altitude_progress
|
|
|
|
# Create realistic Earth background
|
|
create_realistic_earth_background(frame, width, height, current_altitude, center_lat, center_lon, entry_progress)
|
|
|
|
# Add realistic cloud layers
|
|
add_realistic_cloud_layers(frame, width, height, current_altitude, entry_progress)
|
|
|
|
# Draw geographic features (landmasses, coastlines, rivers)
|
|
draw_geographic_features(frame, width, height, center_lat, center_lon, current_altitude, entry_progress)
|
|
|
|
# Add route visualization that becomes more detailed as we descend
|
|
if entry_progress > 0.4: # Route becomes visible halfway through descent
|
|
draw_route_from_space(frame, min_lat, max_lat, min_lon, max_lon, center_lat, center_lon,
|
|
width, height, current_altitude, entry_progress)
|
|
|
|
# Add realistic atmospheric effects
|
|
add_space_atmospheric_effects(frame, width, height, current_altitude, entry_progress)
|
|
|
|
# Add professional UI with realistic information
|
|
add_space_entry_professional_ui(frame, current_altitude, entry_progress, start_pos, width, height)
|
|
|
|
# Add Earth's terminator line (day/night boundary) if at high altitude
|
|
if current_altitude > 20000:
|
|
add_earth_terminator(frame, width, height, current_altitude)
|
|
|
|
return frame
|
|
|
|
def create_realistic_earth_background(frame, width, height, altitude, center_lat, center_lon, progress):
|
|
"""Create realistic Earth background based on altitude"""
|
|
|
|
if altitude > 30000: # Space view - show Earth as sphere
|
|
create_earth_sphere_view(frame, width, height, altitude, center_lat, center_lon)
|
|
elif altitude > 15000: # High atmosphere - curved horizon
|
|
create_curved_horizon_view(frame, width, height, altitude, center_lat, center_lon)
|
|
else: # Lower atmosphere - flat perspective
|
|
create_flat_earth_view(frame, width, height, altitude, center_lat, center_lon, progress)
|
|
|
|
def create_earth_sphere_view(frame, width, height, altitude, center_lat, center_lon):
|
|
"""Create spherical Earth view for space altitudes"""
|
|
|
|
# Space background - deep black with stars
|
|
frame[:] = (5, 5, 15) # Very dark blue-black
|
|
|
|
# Add stars
|
|
np.random.seed(42) # Consistent star pattern
|
|
for _ in range(200):
|
|
x = np.random.randint(0, width)
|
|
y = np.random.randint(0, height // 2) # Stars only in upper half
|
|
brightness = np.random.randint(100, 255)
|
|
frame[y, x] = (brightness, brightness, brightness)
|
|
|
|
# Earth sphere
|
|
earth_radius = min(width, height) // 3
|
|
earth_center_x = width // 2
|
|
earth_center_y = int(height * 0.7) # Earth in lower portion
|
|
|
|
# Create Earth disk
|
|
y_coords, x_coords = np.ogrid[:height, :width]
|
|
earth_mask = (x_coords - earth_center_x)**2 + (y_coords - earth_center_y)**2 <= earth_radius**2
|
|
|
|
# Earth base colors (blue oceans, green/brown land)
|
|
earth_colors = create_earth_surface_colors(width, height, earth_center_x, earth_center_y, earth_radius)
|
|
frame[earth_mask] = earth_colors[earth_mask]
|
|
|
|
# Add Earth's atmospheric glow
|
|
add_earth_atmospheric_glow(frame, earth_center_x, earth_center_y, earth_radius, width, height)
|
|
|
|
def create_curved_horizon_view(frame, width, height, altitude, center_lat, center_lon):
|
|
"""Create curved horizon view for high atmosphere"""
|
|
|
|
# Sky gradient from space to atmosphere
|
|
for y in range(height):
|
|
if y < height * 0.3: # Upper atmosphere/space
|
|
intensity = y / (height * 0.3)
|
|
r = int(5 + (50 - 5) * intensity)
|
|
g = int(10 + (80 - 10) * intensity)
|
|
b = int(25 + (120 - 25) * intensity)
|
|
else: # Lower atmosphere
|
|
intensity = (y - height * 0.3) / (height * 0.7)
|
|
r = int(50 + (135 - 50) * intensity)
|
|
g = int(80 + (206 - 80) * intensity)
|
|
b = int(120 + (235 - 120) * intensity)
|
|
|
|
frame[y, :] = (b, g, r)
|
|
|
|
# Curved horizon line
|
|
horizon_y = int(height * 0.6)
|
|
curvature = altitude / 1000 # More curvature at higher altitude
|
|
|
|
for x in range(width):
|
|
curve_offset = int(curvature * math.sin(math.pi * x / width))
|
|
curve_y = horizon_y + curve_offset
|
|
|
|
# Earth surface below horizon
|
|
if curve_y < height:
|
|
earth_surface_color = get_earth_surface_color(x, curve_y, center_lat, center_lon, width, height)
|
|
for y in range(curve_y, height):
|
|
if y < height:
|
|
frame[y, x] = earth_surface_color
|
|
|
|
def create_flat_earth_view(frame, width, height, altitude, center_lat, center_lon, progress):
|
|
"""Create flat Earth perspective for lower altitudes"""
|
|
|
|
# Realistic sky gradient
|
|
for y in range(int(height * 0.4)):
|
|
sky_intensity = y / (height * 0.4)
|
|
r = int(135 + (200 - 135) * sky_intensity)
|
|
g = int(206 + (230 - 206) * sky_intensity)
|
|
b = int(235 + (255 - 235) * sky_intensity)
|
|
frame[y, :] = (b, g, r)
|
|
|
|
# Terrain with realistic colors and textures
|
|
terrain_start_y = int(height * 0.4)
|
|
for y in range(terrain_start_y, height):
|
|
for x in range(width):
|
|
terrain_color = generate_realistic_terrain_color(x, y, center_lat, center_lon, width, height, altitude)
|
|
frame[y, x] = terrain_color
|
|
|
|
def add_realistic_cloud_layers(frame, width, height, altitude, progress):
|
|
"""Add realistic cloud formations"""
|
|
|
|
if altitude < 30000: # Clouds visible below 30km
|
|
cloud_density = max(0.1, 1.0 - altitude / 30000)
|
|
|
|
# Generate cloud layer using Perlin-like noise
|
|
for y in range(int(height * 0.3), int(height * 0.7)):
|
|
for x in range(0, width, 4): # Sample every 4 pixels for performance
|
|
cloud_noise = (
|
|
math.sin(x * 0.02 + progress * 10) *
|
|
math.sin(y * 0.03 + progress * 8) *
|
|
math.sin((x + y) * 0.01 + progress * 5)
|
|
)
|
|
|
|
cloud_intensity = max(0, cloud_noise * cloud_density)
|
|
if cloud_intensity > 0.3:
|
|
cloud_alpha = min(0.6, cloud_intensity)
|
|
cloud_color = (255, 255, 255)
|
|
|
|
# Blend cloud with existing background
|
|
for dx in range(4):
|
|
if x + dx < width:
|
|
current_color = frame[y, x + dx]
|
|
blended = [
|
|
int(current_color[i] * (1 - cloud_alpha) + cloud_color[i] * cloud_alpha)
|
|
for i in range(3)
|
|
]
|
|
frame[y, x + dx] = tuple(blended)
|
|
|
|
def draw_geographic_features(frame, width, height, center_lat, center_lon, altitude, progress):
|
|
"""Draw realistic geographic features like coastlines and rivers"""
|
|
|
|
if altitude < 20000: # Geographic features visible below 20km
|
|
detail_level = 1.0 - (altitude / 20000)
|
|
|
|
# Simulate coastlines using fractal-like patterns
|
|
coastline_points = generate_coastline_pattern(center_lat, center_lon, width, height, detail_level)
|
|
|
|
for points in coastline_points:
|
|
if len(points) > 1:
|
|
# Draw coastline
|
|
for i in range(len(points) - 1):
|
|
cv2.line(frame, points[i], points[i + 1], (139, 69, 19), 2) # Brown coastline
|
|
|
|
# Add major rivers if detail level is high enough
|
|
if detail_level > 0.5:
|
|
river_points = generate_river_pattern(center_lat, center_lon, width, height, detail_level)
|
|
for points in river_points:
|
|
if len(points) > 1:
|
|
for i in range(len(points) - 1):
|
|
cv2.line(frame, points[i], points[i + 1], (255, 178, 102), 1) # Blue rivers
|
|
|
|
def create_earth_surface_colors(width, height, center_x, center_y, radius):
|
|
"""Generate realistic Earth surface colors"""
|
|
colors = np.zeros((height, width, 3), dtype=np.uint8)
|
|
|
|
for y in range(height):
|
|
for x in range(width):
|
|
dist_from_center = math.sqrt((x - center_x)**2 + (y - center_y)**2)
|
|
if dist_from_center <= radius:
|
|
# Use longitude/latitude to determine terrain type
|
|
angle = math.atan2(y - center_y, x - center_x)
|
|
|
|
# Simulate different terrain types
|
|
terrain_noise = (
|
|
math.sin(angle * 3) *
|
|
math.sin(dist_from_center * 0.1) *
|
|
math.cos(angle * 2 + dist_from_center * 0.05)
|
|
)
|
|
|
|
if terrain_noise > 0.3:
|
|
# Land - green/brown
|
|
colors[y, x] = (34, 139, 34) # Forest green
|
|
elif terrain_noise > 0:
|
|
# Land - brown/tan
|
|
colors[y, x] = (160, 82, 45) # Saddle brown
|
|
else:
|
|
# Ocean - blue
|
|
colors[y, x] = (139, 0, 0) # Dark blue
|
|
|
|
return colors
|
|
|
|
def create_space_sky_background(frame, width, height, altitude):
|
|
"""Create background that transitions from space black to sky blue"""
|
|
# Space to atmosphere transition
|
|
if altitude > 20000:
|
|
# Space: black to deep blue
|
|
space_factor = min(1.0, (altitude - 20000) / 30000)
|
|
for y in range(height):
|
|
intensity = y / height
|
|
r = int(5 * (1 - space_factor) + 0 * space_factor)
|
|
g = int(15 * (1 - space_factor) + 0 * space_factor)
|
|
b = int(30 * (1 - space_factor) + 0 * space_factor)
|
|
frame[y, :] = (b, g, r)
|
|
else:
|
|
# Atmosphere: blue gradient
|
|
for y in range(int(height * 0.6)): # Sky portion
|
|
sky_intensity = y / (height * 0.6)
|
|
r = int(135 + (200 - 135) * sky_intensity)
|
|
g = int(206 + (230 - 206) * sky_intensity)
|
|
b = int(235 + (255 - 235) * sky_intensity)
|
|
frame[y, :] = (b, g, r)
|
|
|
|
# Terrain visible below
|
|
terrain_start_y = int(height * 0.6)
|
|
for y in range(terrain_start_y, height):
|
|
distance_factor = (y - terrain_start_y) / (height - terrain_start_y)
|
|
base_r = int(80 + 60 * distance_factor)
|
|
base_g = int(120 + 80 * distance_factor)
|
|
base_b = int(60 + 40 * distance_factor)
|
|
frame[y, :] = (base_b, base_g, base_r)
|
|
|
|
def draw_earth_curvature(frame, width, height, altitude):
|
|
"""Draw Earth's curvature at high altitudes"""
|
|
if altitude < 15000:
|
|
return
|
|
|
|
# Calculate curvature based on altitude
|
|
curve_factor = min(1.0, (altitude - 15000) / 35000)
|
|
|
|
# Draw curved horizon
|
|
horizon_y = int(height * 0.5)
|
|
curve_amplitude = int(50 * curve_factor)
|
|
|
|
for x in range(width):
|
|
# Sine wave for curvature
|
|
curve_offset = int(curve_amplitude * math.sin(math.pi * x / width))
|
|
curve_y = horizon_y + curve_offset
|
|
|
|
# Draw atmospheric glow around Earth
|
|
for glow_y in range(max(0, curve_y - 20), min(height, curve_y + 5)):
|
|
glow_intensity = 1.0 - abs(glow_y - curve_y) / 20.0
|
|
if glow_intensity > 0:
|
|
# Use numpy operations to prevent overflow
|
|
current_pixel = frame[glow_y, x].astype(np.int32) # Convert to int32 to prevent overflow
|
|
glow_r = int(100 * glow_intensity)
|
|
glow_g = int(150 * glow_intensity)
|
|
glow_b = int(200 * glow_intensity)
|
|
|
|
frame[glow_y, x] = (
|
|
min(255, max(0, current_pixel[0] + glow_r)),
|
|
min(255, max(0, current_pixel[1] + glow_g)),
|
|
min(255, max(0, current_pixel[2] + glow_b))
|
|
)
|
|
|
|
def draw_terrain_from_altitude(frame, camera_lat, camera_lon, view_radius_km,
|
|
width, height, altitude, progress):
|
|
"""Draw terrain detail that increases as altitude decreases"""
|
|
if altitude > 10000:
|
|
# High altitude: show landmass outlines
|
|
draw_landmass_outlines(frame, camera_lat, camera_lon, view_radius_km, width, height)
|
|
else:
|
|
# Lower altitude: show detailed terrain
|
|
detail_factor = 1.0 - (altitude / 10000)
|
|
draw_detailed_terrain(frame, camera_lat, camera_lon, view_radius_km,
|
|
width, height, detail_factor)
|
|
|
|
def draw_landmass_outlines(frame, camera_lat, camera_lon, view_radius_km, width, height):
|
|
"""Draw simplified landmass outlines for space view"""
|
|
# Simplified representation - in real implementation you'd use actual geographic data
|
|
center_x, center_y = width // 2, height // 2
|
|
|
|
# Draw some landmass shapes
|
|
for i in range(5):
|
|
angle = i * 72 # 360/5 degrees
|
|
radius = int(100 + 50 * math.sin(angle * math.pi / 180))
|
|
land_x = center_x + int(radius * math.cos(math.radians(angle)))
|
|
land_y = center_y + int(radius * math.sin(math.radians(angle)))
|
|
|
|
# Draw landmass blob
|
|
cv2.circle(frame, (land_x, land_y), 30, (139, 69, 19), -1) # Brown landmass
|
|
|
|
def draw_detailed_terrain(frame, camera_lat, camera_lon, view_radius_km,
|
|
width, height, detail_factor):
|
|
"""Draw detailed terrain features"""
|
|
# Create terrain texture
|
|
for y in range(height):
|
|
for x in range(width):
|
|
# Generate terrain using noise
|
|
noise1 = math.sin(x * 0.01 * detail_factor) * math.sin(y * 0.01 * detail_factor)
|
|
noise2 = math.sin(x * 0.05 * detail_factor) * math.sin(y * 0.03 * detail_factor)
|
|
|
|
terrain_height = (noise1 + noise2) * 0.5
|
|
|
|
# Color based on terrain height
|
|
if terrain_height > 0.3:
|
|
# Mountains - grey/brown
|
|
color = (100, 120, 140)
|
|
elif terrain_height > 0:
|
|
# Hills - green
|
|
color = (60, 140, 80)
|
|
else:
|
|
# Valleys/water - blue
|
|
color = (120, 100, 60)
|
|
|
|
frame[y, x] = color
|
|
|
|
def draw_route_overview_from_space(frame, min_lat, max_lat, min_lon, max_lon,
|
|
camera_lat, camera_lon, view_radius_km,
|
|
width, height, progress):
|
|
"""Draw route overview visible from space"""
|
|
# Simple route line for space view
|
|
# Map route bounds to screen coordinates
|
|
route_width = max_lon - min_lon
|
|
route_height = max_lat - min_lat
|
|
|
|
if route_width == 0 or route_height == 0:
|
|
return
|
|
|
|
# Calculate route position on screen
|
|
lat_offset = (min_lat + max_lat) / 2 - camera_lat
|
|
lon_offset = (min_lon + max_lon) / 2 - camera_lon
|
|
|
|
# Convert to screen coordinates (simplified)
|
|
route_x = int(width / 2 + lon_offset * width / 2)
|
|
route_y = int(height / 2 + lat_offset * height / 2)
|
|
|
|
route_screen_width = int(route_width * width / 4)
|
|
route_screen_height = int(route_height * height / 4)
|
|
|
|
# Draw route area highlight
|
|
if (0 < route_x < width and 0 < route_y < height):
|
|
# Pulsing route highlight
|
|
pulse = int(20 + 10 * math.sin(progress * 10))
|
|
cv2.rectangle(frame,
|
|
(route_x - route_screen_width, route_y - route_screen_height),
|
|
(route_x + route_screen_width, route_y + route_screen_height),
|
|
(0, 255, 255), 2) # Cyan highlight
|
|
|
|
def add_space_entry_ui(frame, altitude, progress, width, height):
|
|
"""Add UI elements for space entry sequence"""
|
|
# Altitude indicator
|
|
altitude_text = f"Altitude: {altitude/1000:.1f} km"
|
|
cv2.putText(frame, altitude_text, (20, 50),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
|
|
|
|
# Entry progress
|
|
progress_text = f"Descent: {progress*100:.0f}%"
|
|
cv2.putText(frame, progress_text, (20, 90),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
|
|
|
|
# "Approaching Route" text when near the end
|
|
if progress > 0.7:
|
|
cv2.putText(frame, "Approaching Route...", (width//2 - 120, height//2),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
|
|
|
|
def add_atmospheric_glow(frame, width, height, altitude):
|
|
"""Add atmospheric glow effect"""
|
|
if altitude > 5000:
|
|
# Create atmospheric glow overlay
|
|
glow_intensity = min(0.3, altitude / 50000)
|
|
|
|
# Horizontal glow bands
|
|
for y in range(height):
|
|
distance_from_horizon = abs(y - height // 2) / (height // 2)
|
|
if distance_from_horizon < 0.5:
|
|
glow = int(50 * glow_intensity * (1 - distance_from_horizon * 2))
|
|
# Use numpy operations to safely add glow without overflow
|
|
current_blue = frame[y, :, 2].astype(np.int32)
|
|
frame[y, :, 2] = np.clip(current_blue + glow, 0, 255).astype(np.uint8)
|
|
|
|
def ensure_safe_pixel_value(value):
|
|
"""Ensure pixel values are within valid range (0-255)"""
|
|
return max(0, min(255, int(value)))
|
|
|
|
def safe_frame_assignment(frame, y, x, color):
|
|
"""Safely assign color values to frame to prevent overflow"""
|
|
if isinstance(color, (list, tuple)) and len(color) == 3:
|
|
frame[y, x] = (
|
|
ensure_safe_pixel_value(color[0]),
|
|
ensure_safe_pixel_value(color[1]),
|
|
ensure_safe_pixel_value(color[2])
|
|
)
|
|
else:
|
|
# Single value color
|
|
safe_value = ensure_safe_pixel_value(color)
|
|
frame[y, x] = (safe_value, safe_value, safe_value)
|
|
|
|
def create_transition_bridge_frame(start_pos, center_lat, center_lon, min_lat, max_lat, min_lon, max_lon,
|
|
width, height):
|
|
"""
|
|
Create a smooth transition frame that bridges space entry to route following
|
|
"""
|
|
# This is essentially the first route frame but with a slightly higher camera position
|
|
# to create a smooth transition from the space entry descent
|
|
|
|
# Create canvas
|
|
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
|
|
# Transition camera parameters (between space entry end and route start)
|
|
camera_height = 2500 # Slightly higher than normal route following
|
|
camera_lat = start_pos['latitude']
|
|
camera_lon = start_pos['longitude']
|
|
|
|
# Create sky background (transition from space-like to normal sky)
|
|
for y in range(int(height * 0.5)): # Sky portion
|
|
sky_intensity = y / (height * 0.5)
|
|
r = int(100 + (200 - 100) * sky_intensity)
|
|
g = int(150 + (230 - 150) * sky_intensity)
|
|
b = int(200 + (255 - 200) * sky_intensity)
|
|
frame[y, :] = (b, g, r)
|
|
|
|
# Terrain background
|
|
terrain_start_y = int(height * 0.5)
|
|
for y in range(terrain_start_y, height):
|
|
distance_factor = (y - terrain_start_y) / (height - terrain_start_y)
|
|
base_r = int(80 + 60 * distance_factor)
|
|
base_g = int(120 + 80 * distance_factor)
|
|
base_b = int(60 + 40 * distance_factor)
|
|
frame[y, :] = (base_b, base_g, base_r)
|
|
|
|
# Add route start indicator
|
|
route_center_x = width // 2
|
|
route_center_y = int(height * 0.7)
|
|
|
|
# Draw route start marker
|
|
cv2.circle(frame, (route_center_x, route_center_y), 20, (0, 255, 255), 3)
|
|
cv2.circle(frame, (route_center_x, route_center_y), 8, (255, 255, 255), -1)
|
|
|
|
# Add "Route Starting" text
|
|
cv2.putText(frame, "Route Starting...", (width//2 - 100, height//2 + 100),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2)
|
|
|
|
return frame
|
|
|
|
def get_earth_surface_color(x, y, center_lat, center_lon, width, height):
|
|
"""Get realistic Earth surface color for a given screen position"""
|
|
|
|
# Convert screen position to geographic simulation
|
|
terrain_noise = (
|
|
math.sin(x * 0.01) * math.sin(y * 0.01) +
|
|
math.sin(x * 0.05) * math.sin(y * 0.03) +
|
|
math.sin(x * 0.02 + y * 0.02)
|
|
)
|
|
|
|
if terrain_noise > 0.5:
|
|
# Mountains/hills - brown/gray
|
|
return (101, 67, 33)
|
|
elif terrain_noise > 0:
|
|
# Plains/grassland - green
|
|
return (34, 139, 34)
|
|
elif terrain_noise > -0.3:
|
|
# Desert/dry land - tan
|
|
return (194, 178, 128)
|
|
else:
|
|
# Water - blue
|
|
return (65, 105, 225)
|
|
|
|
def generate_realistic_terrain_color(x, y, center_lat, center_lon, width, height, altitude):
|
|
"""Generate realistic terrain color with proper texturing"""
|
|
|
|
# Multiple noise layers for realistic terrain
|
|
noise1 = math.sin(x * 0.005) * math.sin(y * 0.005)
|
|
noise2 = math.sin(x * 0.02) * math.sin(y * 0.02) * 0.5
|
|
noise3 = math.sin(x * 0.1) * math.sin(y * 0.1) * 0.2
|
|
|
|
combined_noise = noise1 + noise2 + noise3
|
|
|
|
# Distance from center affects terrain type
|
|
center_x, center_y = width // 2, height // 2
|
|
distance_from_center = math.sqrt((x - center_x)**2 + (y - center_y)**2) / min(width, height)
|
|
|
|
# Terrain classification based on noise and position
|
|
if combined_noise > 0.7:
|
|
# High mountains - gray/white
|
|
base_color = (169, 169, 169)
|
|
elif combined_noise > 0.3:
|
|
# Hills/forests - green
|
|
base_color = (34, 139, 34)
|
|
elif combined_noise > 0:
|
|
# Plains - light green
|
|
base_color = (124, 252, 0)
|
|
elif combined_noise > -0.3:
|
|
# Desert/dry areas - tan
|
|
base_color = (210, 180, 140)
|
|
else:
|
|
# Water/wetlands - blue
|
|
base_color = (65, 105, 225)
|
|
|
|
# Add altitude-based atmospheric perspective
|
|
distance_factor = min(1.0, (y - height * 0.4) / (height * 0.6))
|
|
atmosphere_factor = 1.0 - (distance_factor * 0.3)
|
|
|
|
final_color = tuple(int(c * atmosphere_factor) for c in base_color)
|
|
return final_color
|
|
|
|
def generate_coastline_pattern(center_lat, center_lon, width, height, detail_level):
|
|
"""Generate realistic coastline patterns"""
|
|
coastlines = []
|
|
|
|
if detail_level > 0.3:
|
|
# Generate several coastline segments
|
|
for coast_id in range(3):
|
|
points = []
|
|
start_x = int(width * (0.1 + coast_id * 0.3))
|
|
start_y = int(height * 0.6)
|
|
|
|
# Generate fractal-like coastline
|
|
for i in range(20):
|
|
noise_x = int(20 * math.sin(i * 0.5 + coast_id) * detail_level)
|
|
noise_y = int(10 * math.cos(i * 0.3 + coast_id) * detail_level)
|
|
|
|
x = start_x + i * 30 + noise_x
|
|
y = start_y + noise_y
|
|
|
|
if 0 <= x < width and 0 <= y < height:
|
|
points.append((x, y))
|
|
|
|
if len(points) > 1:
|
|
coastlines.append(points)
|
|
|
|
return coastlines
|
|
|
|
def generate_river_pattern(center_lat, center_lon, width, height, detail_level):
|
|
"""Generate realistic river patterns"""
|
|
rivers = []
|
|
|
|
if detail_level > 0.6:
|
|
# Generate a few major rivers
|
|
for river_id in range(2):
|
|
points = []
|
|
start_x = int(width * (0.2 + river_id * 0.6))
|
|
start_y = int(height * 0.3)
|
|
|
|
# Rivers flow generally downward with meanders
|
|
for i in range(15):
|
|
meander = int(15 * math.sin(i * 0.8 + river_id * 2))
|
|
x = start_x + meander
|
|
y = start_y + i * 25
|
|
|
|
if 0 <= x < width and 0 <= y < height:
|
|
points.append((x, y))
|
|
|
|
if len(points) > 1:
|
|
rivers.append(points)
|
|
|
|
return rivers
|
|
|
|
def add_earth_atmospheric_glow(frame, center_x, center_y, radius, width, height):
|
|
"""Add realistic atmospheric glow around Earth"""
|
|
|
|
for y in range(max(0, center_y - radius - 50), min(height, center_y + radius + 50)):
|
|
for x in range(max(0, center_x - radius - 50), min(width, center_x + radius + 50)):
|
|
distance = math.sqrt((x - center_x)**2 + (y - center_y)**2)
|
|
|
|
if radius < distance < radius + 40:
|
|
# Atmospheric glow
|
|
glow_intensity = 1.0 - (distance - radius) / 40.0
|
|
if glow_intensity > 0:
|
|
glow_blue = int(100 * glow_intensity)
|
|
current_color = frame[y, x].astype(np.int32)
|
|
frame[y, x] = (
|
|
np.clip(current_color[0] + glow_blue // 3, 0, 255),
|
|
np.clip(current_color[1] + glow_blue // 2, 0, 255),
|
|
np.clip(current_color[2] + glow_blue, 0, 255)
|
|
)
|
|
|
|
def draw_route_from_space(frame, min_lat, max_lat, min_lon, max_lon, camera_lat, camera_lon,
|
|
width, height, altitude, progress):
|
|
"""Draw route visualization visible from space with realistic styling"""
|
|
|
|
# Calculate route bounds on screen
|
|
lat_center = (min_lat + max_lat) / 2
|
|
lon_center = (min_lon + max_lon) / 2
|
|
|
|
# Project to screen coordinates
|
|
lat_offset = (lat_center - camera_lat) * 100000 / altitude
|
|
lon_offset = (lon_center - camera_lon) * 100000 / altitude
|
|
|
|
route_x = int(width / 2 + lon_offset)
|
|
route_y = int(height / 2 + lat_offset)
|
|
|
|
if 0 < route_x < width and 0 < route_y < height:
|
|
# Route area highlight - pulsing effect
|
|
pulse = 1.0 + 0.3 * math.sin(progress * 20)
|
|
route_size = int(20 * pulse)
|
|
|
|
# Outer glow
|
|
cv2.circle(frame, (route_x, route_y), route_size + 10, (100, 100, 255), 2)
|
|
# Main route indicator
|
|
cv2.circle(frame, (route_x, route_y), route_size, (255, 255, 100), 3)
|
|
# Inner core
|
|
cv2.circle(frame, (route_x, route_y), route_size // 2, (255, 255, 255), -1)
|
|
|
|
# Route path preview (simplified)
|
|
route_length = int(40 + progress * 60)
|
|
for i in range(5):
|
|
angle = i * math.pi / 2
|
|
end_x = route_x + int(route_length * math.cos(angle))
|
|
end_y = route_y + int(route_length * math.sin(angle))
|
|
cv2.line(frame, (route_x, route_y), (end_x, end_y), (255, 200, 100), 2)
|
|
|
|
def add_space_atmospheric_effects(frame, width, height, altitude, progress):
|
|
"""Add realistic atmospheric effects based on altitude"""
|
|
|
|
if altitude > 25000:
|
|
# Space effects - stars and cosmic background
|
|
add_star_field(frame, width, height, altitude)
|
|
|
|
if 5000 < altitude < 30000:
|
|
# Atmospheric scattering effects
|
|
add_atmospheric_scattering(frame, width, height, altitude)
|
|
|
|
# Earth's limb glow at high altitudes
|
|
if altitude > 15000:
|
|
add_earth_limb_glow(frame, width, height, altitude)
|
|
|
|
def add_star_field(frame, width, height, altitude):
|
|
"""Add realistic star field for space views"""
|
|
|
|
star_intensity = min(1.0, (altitude - 25000) / 25000)
|
|
num_stars = int(300 * star_intensity)
|
|
|
|
np.random.seed(42) # Consistent star pattern
|
|
for _ in range(num_stars):
|
|
x = np.random.randint(0, width)
|
|
y = np.random.randint(0, height // 2) # Stars mainly in upper half
|
|
|
|
# Vary star brightness and size
|
|
brightness = np.random.randint(150, 255)
|
|
size = np.random.choice([1, 1, 1, 2], p=[0.7, 0.2, 0.05, 0.05])
|
|
|
|
if size == 1:
|
|
frame[y, x] = (brightness, brightness, brightness)
|
|
else:
|
|
cv2.circle(frame, (x, y), size, (brightness, brightness, brightness), -1)
|
|
|
|
def add_atmospheric_scattering(frame, width, height, altitude):
|
|
"""Add atmospheric scattering effects"""
|
|
|
|
scattering_intensity = 1.0 - (altitude / 30000)
|
|
|
|
# Blue scattering in upper atmosphere
|
|
for y in range(0, int(height * 0.5)):
|
|
scatter_factor = scattering_intensity * (1.0 - y / (height * 0.5))
|
|
blue_scatter = int(20 * scatter_factor)
|
|
|
|
for x in range(0, width, 4): # Sample for performance
|
|
current_color = frame[y, x:x+4]
|
|
frame[y, x:x+4] = np.minimum(255, current_color + [blue_scatter//2, blue_scatter//2, blue_scatter])
|
|
|
|
def add_earth_limb_glow(frame, width, height, altitude):
|
|
"""Add Earth's limb glow effect"""
|
|
|
|
limb_intensity = min(1.0, (altitude - 15000) / 20000)
|
|
|
|
# Horizontal glow band representing Earth's atmosphere
|
|
glow_y = int(height * 0.7)
|
|
glow_height = int(30 * limb_intensity)
|
|
|
|
for y in range(max(0, glow_y - glow_height), min(height, glow_y + glow_height)):
|
|
glow_factor = 1.0 - abs(y - glow_y) / glow_height
|
|
blue_glow = int(80 * glow_factor * limb_intensity)
|
|
|
|
frame[y, :, 2] = np.minimum(255, frame[y, :, 2] + blue_glow)
|
|
|
|
def add_earth_terminator(frame, width, height, altitude):
|
|
"""Add Earth's day/night terminator line"""
|
|
|
|
if altitude > 20000:
|
|
# Diagonal terminator line
|
|
terminator_angle = math.pi / 6 # 30 degrees
|
|
|
|
for y in range(height):
|
|
terminator_x = int(width * 0.3 + y * math.tan(terminator_angle))
|
|
|
|
if 0 < terminator_x < width:
|
|
# Darken the night side
|
|
for x in range(0, terminator_x):
|
|
current_color = frame[y, x].astype(np.int32)
|
|
darkened = current_color * 0.3 # 70% darker
|
|
frame[y, x] = np.clip(darkened, 0, 255).astype(np.uint8)
|
|
|
|
# Add terminator glow
|
|
glow_width = 20
|
|
for dx in range(-glow_width, glow_width):
|
|
glow_x = terminator_x + dx
|
|
if 0 <= glow_x < width:
|
|
glow_intensity = 1.0 - abs(dx) / glow_width
|
|
orange_glow = int(50 * glow_intensity)
|
|
current_color = frame[y, glow_x].astype(np.int32)
|
|
frame[y, glow_x] = np.clip(current_color + [orange_glow//2, orange_glow//2, orange_glow], 0, 255)
|
|
|
|
def add_space_entry_professional_ui(frame, altitude, progress, start_pos, width, height):
|
|
"""Add professional Google Earth-style UI for space entry"""
|
|
|
|
# Create semi-transparent overlay panels
|
|
overlay = frame.copy()
|
|
|
|
# Main information panel (top-left)
|
|
panel_width = 320
|
|
panel_height = 140
|
|
cv2.rectangle(overlay, (20, 20), (panel_width, panel_height), (40, 40, 40), -1)
|
|
cv2.addWeighted(overlay, 0.8, frame, 0.2, 0, frame)
|
|
|
|
# Panel border with gradient effect
|
|
cv2.rectangle(frame, (20, 20), (panel_width, panel_height), (200, 200, 200), 2)
|
|
cv2.rectangle(frame, (22, 22), (panel_width-2, panel_height-2), (100, 100, 100), 1)
|
|
|
|
# Altitude display (large, prominent)
|
|
altitude_text = f"{altitude/1000:.1f} km"
|
|
cv2.putText(frame, "ALTITUDE", (30, 45), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (150, 150, 150), 1)
|
|
cv2.putText(frame, altitude_text, (30, 75), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 255, 255), 2)
|
|
|
|
# Descent progress
|
|
progress_text = f"DESCENT: {progress*100:.0f}%"
|
|
cv2.putText(frame, progress_text, (30, 105), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (100, 255, 100), 2)
|
|
|
|
# Coordinates
|
|
coord_text = f"LAT: {start_pos['latitude']:.4f}"
|
|
cv2.putText(frame, coord_text, (30, 125), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
|
coord_text = f"LON: {start_pos['longitude']:.4f}"
|
|
cv2.putText(frame, coord_text, (30, 145), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
|
|
|
# Speed indicator (top-right)
|
|
speed_panel_x = width - 250
|
|
cv2.rectangle(overlay, (speed_panel_x, 20), (width - 20, 100), (40, 40, 40), -1)
|
|
cv2.addWeighted(overlay, 0.8, frame, 0.2, 0, frame)
|
|
cv2.rectangle(frame, (speed_panel_x, 20), (width - 20, 100), (200, 200, 200), 2)
|
|
|
|
descent_speed = int((1 - progress) * 15000 + 500) # Simulated descent speed
|
|
cv2.putText(frame, "DESCENT RATE", (speed_panel_x + 10, 45), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (150, 150, 150), 1)
|
|
cv2.putText(frame, f"{descent_speed} m/min", (speed_panel_x + 10, 75), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 100, 100), 2)
|
|
|
|
# Entry status message (center)
|
|
if progress < 0.3:
|
|
status_text = "INITIATING DESCENT FROM SPACE"
|
|
text_color = (100, 200, 255)
|
|
elif progress < 0.7:
|
|
status_text = "ENTERING EARTH'S ATMOSPHERE"
|
|
text_color = (255, 200, 100)
|
|
else:
|
|
status_text = "APPROACHING ROUTE START POINT"
|
|
text_color = (100, 255, 100)
|
|
|
|
text_size = cv2.getTextSize(status_text, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2)[0]
|
|
text_x = (width - text_size[0]) // 2
|
|
text_y = height - 80
|
|
|
|
# Text background
|
|
cv2.rectangle(frame, (text_x - 10, text_y - 35), (text_x + text_size[0] + 10, text_y + 10), (0, 0, 0), -1)
|
|
cv2.putText(frame, status_text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.8, text_color, 2)
|
|
|
|
# Progress bar (bottom)
|
|
progress_bar_width = width - 100
|
|
progress_bar_height = 8
|
|
progress_bar_x = 50
|
|
progress_bar_y = height - 40
|
|
|
|
# Background
|
|
cv2.rectangle(frame, (progress_bar_x, progress_bar_y),
|
|
(progress_bar_x + progress_bar_width, progress_bar_y + progress_bar_height),
|
|
(60, 60, 60), -1)
|
|
|
|
# Progress fill with color gradient
|
|
progress_width = int(progress_bar_width * progress)
|
|
if progress_width > 0:
|
|
# Create gradient effect
|
|
for x in range(progress_width):
|
|
color_factor = x / progress_width if progress_width > 0 else 0
|
|
r = int(255 * (1 - color_factor) + 100 * color_factor)
|
|
g = int(100 * (1 - color_factor) + 255 * color_factor)
|
|
b = 100
|
|
|
|
cv2.rectangle(frame, (progress_bar_x + x, progress_bar_y),
|
|
(progress_bar_x + x + 1, progress_bar_y + progress_bar_height),
|
|
(b, g, r), -1)
|
|
|
|
# Border
|
|
cv2.rectangle(frame, (progress_bar_x, progress_bar_y),
|
|
(progress_bar_x + progress_bar_width, progress_bar_y + progress_bar_height),
|
|
(200, 200, 200), 1)
|