Files
traccar_animation/py_scripts/video_3d_generator.py
2025-07-08 15:26:33 +03:00

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)