import os import json import numpy as np from datetime import datetime, timedelta import time from PIL import Image import cv2 from geopy.distance import geodesic import math # Optional advanced dependencies with fallback handling try: import pandas as pd import geopandas as gpd PANDAS_AVAILABLE = True except ImportError: PANDAS_AVAILABLE = False print("Warning: pandas/geopandas not available. Install with: pip install pandas geopandas") try: import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots PLOTLY_AVAILABLE = True except ImportError: PLOTLY_AVAILABLE = False print("Warning: plotly not available. Install with: pip install plotly") try: import pydeck as pdk PYDECK_AVAILABLE = True except ImportError: PYDECK_AVAILABLE = False print("Warning: pydeck not available. Install with: pip install pydeck") try: import cv2 OPENCV_AVAILABLE = True except ImportError: OPENCV_AVAILABLE = False print("Warning: opencv-python not available. Install with: pip install opencv-python") class NavigationAnimationGenerator: """ Professional navigation animation generator with satellite view and 3D camera following Creates Google Earth-style entry scene and detailed terrain navigation """ def __init__(self, output_folder): self.output_folder = output_folder self.frames_folder = os.path.join(output_folder, "nav_frames") self.temp_folder = os.path.join(output_folder, "temp") # Create necessary folders os.makedirs(self.frames_folder, exist_ok=True) os.makedirs(self.temp_folder, exist_ok=True) # Navigation animation settings self.fps = 30 self.entry_duration = 4 # seconds for Google Earth entry self.camera_height_min = 1000 # meters self.camera_height_max = 2000 # meters self.follow_distance = 500 # meters behind navigation point def check_dependencies(self): """Check if all required dependencies are available""" missing = [] if not PANDAS_AVAILABLE: missing.append("pandas geopandas") if not PLOTLY_AVAILABLE: missing.append("plotly") if not PYDECK_AVAILABLE: missing.append("pydeck") if not OPENCV_AVAILABLE: missing.append("opencv-python") if missing: raise ImportError(f"Missing required dependencies: {', '.join(missing)}. Please install them with: pip install {' '.join(missing)}") return True def load_gps_data(self, positions_file): """Load and preprocess GPS data for navigation""" with open(positions_file, 'r') as f: positions = json.load(f) # Convert to DataFrame df = pd.DataFrame(positions) # Parse timestamps df['timestamp'] = pd.to_datetime(df['fixTime']) df = df.sort_values('timestamp') # Calculate speed and bearing df['speed_kmh'] = df['speed'] * 1.852 # Convert knots to km/h df['elevation'] = df.get('altitude', 100) # Calculate bearings and distances bearings = [] distances = [] for i in range(len(df)): if i == 0: bearings.append(0) distances.append(0) else: # Calculate bearing to next point lat1, lon1 = df.iloc[i-1]['latitude'], df.iloc[i-1]['longitude'] lat2, lon2 = df.iloc[i]['latitude'], df.iloc[i]['longitude'] bearing = self.calculate_bearing(lat1, lon1, lat2, lon2) distance = geodesic((lat1, lon1), (lat2, lon2)).meters bearings.append(bearing) distances.append(distance) df['bearing'] = bearings df['distance'] = distances return df def calculate_bearing(self, lat1, lon1, lat2, lon2): """Calculate bearing between two GPS points""" lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) dlon = lon2 - lon1 y = math.sin(dlon) * math.cos(lat2) x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon) bearing = math.atan2(y, x) bearing = math.degrees(bearing) bearing = (bearing + 360) % 360 return bearing def create_google_earth_entry_scene(self, df, frame_num): """Create Google Earth-style entry scene (zooming in from space)""" if not PLOTLY_AVAILABLE: raise ImportError("Plotly is required for entry scene") # Get route bounds center_lat = df['latitude'].mean() center_lon = df['longitude'].mean() # Entry animation: start from very high altitude and zoom in total_entry_frames = self.entry_duration * self.fps zoom_progress = frame_num / total_entry_frames # Camera altitude decreases exponentially start_altitude = 50000 # Start from 50km end_altitude = 3000 # End at 3km current_altitude = start_altitude * (1 - zoom_progress) + end_altitude * zoom_progress # Create figure fig = go.Figure() # Add Earth-like surface with satellite imagery simulation terrain_size = 0.5 - (0.4 * zoom_progress) # Zoom in effect resolution = int(30 + (50 * zoom_progress)) # More detail as we zoom lat_range = np.linspace(center_lat - terrain_size, center_lat + terrain_size, resolution) lon_range = np.linspace(center_lon - terrain_size, center_lon + terrain_size, resolution) lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range) # Generate satellite-like terrain terrain_heights = self.generate_satellite_terrain(lat_mesh, lon_mesh, center_lat, center_lon) # Add terrain surface fig.add_trace( go.Surface( x=lon_mesh, y=lat_mesh, z=terrain_heights, colorscale=[ [0.0, 'rgb(0,100,0)'], # Deep green (forests) [0.2, 'rgb(34,139,34)'], # Forest green [0.4, 'rgb(255,215,0)'], # Gold (fields) [0.6, 'rgb(139,69,19)'], # Brown (earth) [0.8, 'rgb(105,105,105)'], # Gray (rock) [1.0, 'rgb(255,255,255)'] # White (snow) ], opacity=0.95, showscale=False, lighting=dict( ambient=0.4, diffuse=0.8, specular=0.2 ) ) ) # Show partial route (fading in) route_alpha = min(1.0, zoom_progress * 2) if route_alpha > 0: fig.add_trace( go.Scatter3d( x=df['longitude'], y=df['latitude'], z=df['elevation'] + 100, mode='lines', line=dict( color='red', width=8, ), opacity=route_alpha, name='Route' ) ) # Camera position for entry effect camera_distance = current_altitude / 10000 fig.update_layout( title=dict( text=f'Navigation Overview - Approaching Destination', x=0.5, font=dict(size=28, color='white', family="Arial Black") ), scene=dict( camera=dict( eye=dict(x=0, y=-camera_distance, z=camera_distance), center=dict(x=0, y=0, z=0), up=dict(x=0, y=0, z=1) ), xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False), aspectmode='cube', bgcolor='rgb(0,0,50)', # Space-like background ), paper_bgcolor='black', showlegend=False, width=1920, height=1080, margin=dict(l=0, r=0, t=60, b=0) ) # Save frame frame_path = os.path.join(self.frames_folder, f"NavEntry_{frame_num:04d}.png") fig.write_image(frame_path, engine="kaleido") return frame_path def create_navigation_frame(self, df, current_index, frame_num): """Create detailed navigation frame with 3D following camera""" if not PLOTLY_AVAILABLE: raise ImportError("Plotly is required for navigation frames") current_row = df.iloc[current_index] current_lat = current_row['latitude'] current_lon = current_row['longitude'] current_alt = current_row['elevation'] current_speed = current_row['speed_kmh'] current_bearing = current_row['bearing'] # Get route progress completed_route = df.iloc[:current_index + 1] remaining_route = df.iloc[current_index:] # Create detailed terrain around current position terrain_radius = 0.01 # degrees around current position resolution = 60 lat_range = np.linspace(current_lat - terrain_radius, current_lat + terrain_radius, resolution) lon_range = np.linspace(current_lon - terrain_radius, current_lon + terrain_radius, resolution) lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range) # Generate high-detail satellite terrain terrain_heights = self.generate_detailed_terrain(lat_mesh, lon_mesh, current_lat, current_lon) # Create navigation display figure fig = go.Figure() # Add detailed terrain fig.add_trace( go.Surface( x=lon_mesh, y=lat_mesh, z=terrain_heights, colorscale=[ [0.0, 'rgb(34,139,34)'], # Forest green [0.2, 'rgb(107,142,35)'], # Olive drab [0.4, 'rgb(255,215,0)'], # Gold (fields) [0.5, 'rgb(210,180,140)'], # Tan (roads/clearings) [0.7, 'rgb(139,69,19)'], # Brown (earth) [0.9, 'rgb(105,105,105)'], # Gray (rock) [1.0, 'rgb(255,255,255)'] # White (peaks) ], opacity=0.9, showscale=False, lighting=dict( ambient=0.3, diffuse=0.9, specular=0.1 ) ) ) # Add completed route (green) if len(completed_route) > 1: fig.add_trace( go.Scatter3d( x=completed_route['longitude'], y=completed_route['latitude'], z=completed_route['elevation'] + 50, mode='lines', line=dict(color='lime', width=12), name='Completed' ) ) # Add remaining route (blue, semi-transparent) if len(remaining_route) > 1: fig.add_trace( go.Scatter3d( x=remaining_route['longitude'], y=remaining_route['latitude'], z=remaining_route['elevation'] + 50, mode='lines', line=dict(color='cyan', width=8), opacity=0.6, name='Remaining' ) ) # Add navigation point (current vehicle position) fig.add_trace( go.Scatter3d( x=[current_lon], y=[current_lat], z=[current_alt + 100], mode='markers', marker=dict( color='red', size=25, symbol='diamond', line=dict(color='white', width=4) ), name='Vehicle' ) ) # Add direction indicator bearing_rad = math.radians(current_bearing) arrow_length = 0.002 arrow_end_lat = current_lat + arrow_length * math.cos(bearing_rad) arrow_end_lon = current_lon + arrow_length * math.sin(bearing_rad) fig.add_trace( go.Scatter3d( x=[current_lon, arrow_end_lon], y=[current_lat, arrow_end_lat], z=[current_alt + 120, current_alt + 120], mode='lines', line=dict(color='yellow', width=15), name='Direction' ) ) # Calculate dynamic camera position for 3D following # Camera follows behind and above at specified height camera_height = self.camera_height_min + (self.camera_height_max - self.camera_height_min) * (current_speed / 100) follow_distance_deg = 0.005 # degrees behind vehicle # Position camera behind vehicle based on bearing camera_bearing = (current_bearing + 180) % 360 # Opposite direction camera_bearing_rad = math.radians(camera_bearing) camera_lat = current_lat + follow_distance_deg * math.cos(camera_bearing_rad) camera_lon = current_lon + follow_distance_deg * math.sin(camera_bearing_rad) # Calculate relative camera position camera_eye_x = (camera_lon - current_lon) * 100 camera_eye_y = (camera_lat - current_lat) * 100 camera_eye_z = camera_height / 1000 # Navigation info overlay total_distance = sum(df['distance']) completed_distance = sum(completed_route['distance']) progress_percent = (completed_distance / total_distance) * 100 if total_distance > 0 else 0 fig.update_layout( title=dict( text=f'Navigation • Speed: {current_speed:.1f} km/h • Progress: {progress_percent:.1f}% • {current_row["timestamp"].strftime("%H:%M:%S")}', x=0.5, font=dict(size=20, color='white', family="Arial Black") ), scene=dict( camera=dict( eye=dict(x=camera_eye_x, y=camera_eye_y, z=camera_eye_z), center=dict(x=0, y=0, z=0), up=dict(x=0, y=0, z=1) ), xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False), aspectmode='manual', aspectratio=dict(x=1, y=1, z=0.3), bgcolor='rgb(135,206,235)' # Sky blue ), paper_bgcolor='black', showlegend=False, width=1920, height=1080, margin=dict(l=0, r=0, t=50, b=0) ) # Save frame frame_path = os.path.join(self.frames_folder, f"Navigation_{frame_num:04d}.png") fig.write_image(frame_path, engine="kaleido") return frame_path def generate_satellite_terrain(self, lat_mesh, lon_mesh, center_lat, center_lon): """Generate satellite-view realistic terrain for entry scene""" # Convert to local coordinates lat_m = (lat_mesh - center_lat) * 111000 lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat)) # Base elevation with realistic variation base_height = 200 + 50 * np.sin(lat_m / 5000) * np.cos(lon_m / 3000) # Mountain ranges mountains = 800 * np.exp(-((lat_m - 2000)**2 + (lon_m - 1000)**2) / (3000**2)) mountains += 600 * np.exp(-((lat_m + 1500)**2 + (lon_m + 2000)**2) / (2500**2)) # Hills and valleys hills = 200 * np.sin(lat_m / 1000) * np.cos(lon_m / 1200) valleys = -100 * np.exp(-((lat_m)**2 + (lon_m)**2) / (2000**2)) terrain = base_height + mountains + hills + valleys return np.maximum(terrain, 50) def generate_detailed_terrain(self, lat_mesh, lon_mesh, center_lat, center_lon): """Generate high-detail terrain for navigation view""" # Convert to local coordinates lat_m = (lat_mesh - center_lat) * 111000 lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat)) # Base terrain base = 150 + 30 * np.sin(lat_m / 500) * np.cos(lon_m / 400) # Local features hills = 100 * np.exp(-((lat_m - 300)**2 + (lon_m - 200)**2) / (200**2)) ridges = 80 * np.exp(-((lat_m + 200)**2 + (lon_m - 400)**2) / (300**2)) # Fine detail detail = 20 * np.sin(lat_m / 50) * np.cos(lon_m / 60) terrain = base + hills + ridges + detail return np.maximum(terrain, 30) for i in range(len(df)): if i == 0: distances.append(0) bearings.append(0) else: prev_point = (df.iloc[i-1]['latitude'], df.iloc[i-1]['longitude']) curr_point = (df.iloc[i]['latitude'], df.iloc[i]['longitude']) # Calculate distance dist = geodesic(prev_point, curr_point).meters distances.append(dist) # Calculate bearing lat1, lon1 = math.radians(prev_point[0]), math.radians(prev_point[1]) lat2, lon2 = math.radians(curr_point[0]), math.radians(curr_point[1]) dlon = lon2 - lon1 bearing = math.atan2( math.sin(dlon) * math.cos(lat2), math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon) ) bearings.append(math.degrees(bearing)) df['distance'] = distances df['bearing'] = bearings return df def create_pydeck_frame(self, df, current_index, frame_num): """Create a single frame using Pydeck""" # Get current position current_row = df.iloc[current_index] # Get trail data (previous points) start_idx = max(0, current_index - self.trail_length) trail_data = df.iloc[start_idx:current_index + 1].copy() # Create color gradient for trail (fade effect) trail_colors = [] for i, _ in enumerate(trail_data.iterrows()): alpha = (i + 1) / len(trail_data) * 255 trail_colors.append([255, 100, 100, int(alpha)]) trail_data['color'] = trail_colors # Create the deck view_state = pdk.ViewState( longitude=current_row['longitude'], latitude=current_row['latitude'], zoom=16, pitch=60, bearing=current_row['bearing'] ) # Path layer (trail) path_layer = pdk.Layer( "PathLayer", trail_data, get_path=lambda x: [[x['longitude'], x['latitude'], x['elevation']]], get_color=[255, 100, 100, 200], width_min_pixels=3, width_max_pixels=8, ) # Scatterplot layer (current position) current_point = pd.DataFrame([{ 'longitude': current_row['longitude'], 'latitude': current_row['latitude'], 'elevation': current_row['elevation'] + 10, 'speed': current_row['speed_kmh'] }]) scatter_layer = pdk.Layer( "ScatterplotLayer", current_point, get_position=['longitude', 'latitude', 'elevation'], get_radius=15, get_color=[255, 255, 0, 255], pickable=True ) # Create deck deck = pdk.Deck( layers=[path_layer, scatter_layer], initial_view_state=view_state, map_style='mapbox://styles/mapbox/satellite-v9' ) # Save frame frame_path = os.path.join(self.frames_folder, f"frame_{frame_num:06d}.png") deck.to_html(frame_path.replace('.png', '.html')) return frame_path def create_plotly_frame(self, df, current_index, frame_num): """Create a single frame using Plotly for 3D visualization""" # Get current position current_row = df.iloc[current_index] # Get trail data start_idx = max(0, current_index - self.trail_length) trail_data = df.iloc[start_idx:current_index + 1] # Create 3D scatter plot fig = go.Figure() # Add trail fig.add_trace(go.Scatter3d( x=trail_data['longitude'], y=trail_data['latitude'], z=trail_data['elevation'], mode='lines+markers', line=dict( color=trail_data.index, colorscale='Plasma', width=8 ), marker=dict( size=3, opacity=0.8 ), name='Trail' )) # Add current position fig.add_trace(go.Scatter3d( x=[current_row['longitude']], y=[current_row['latitude']], z=[current_row['elevation'] + 50], mode='markers', marker=dict( size=15, color='red', symbol='diamond' ), name='Current Position' )) # Add speed information as text speed_text = f"Speed: {current_row['speed_kmh']:.1f} km/h
" speed_text += f"Time: {current_row['timestamp'].strftime('%H:%M:%S')}
" speed_text += f"Altitude: {current_row['elevation']:.0f} m" # Update layout fig.update_layout( title=f"3D GPS Track Animation - Frame {frame_num}", scene=dict( xaxis_title='Longitude', yaxis_title='Latitude', zaxis_title='Elevation (m)', camera=dict( eye=dict(x=1.5, y=1.5, z=1.5) ), aspectmode='cube' ), annotations=[ dict( text=speed_text, x=0.02, y=0.98, xref='paper', yref='paper', showarrow=False, bgcolor='rgba(255,255,255,0.8)', bordercolor='black', borderwidth=1 ) ], width=1920, height=1080 ) # Save frame frame_path = os.path.join(self.frames_folder, f"frame_{frame_num:06d}.png") fig.write_image(frame_path, engine="kaleido") return frame_path def create_advanced_plotly_frame(self, df, current_index, frame_num): """Create Relive-style progressive animation frame""" # Progressive track: show all points from start to current position current_track = df.iloc[:current_index + 1] current_row = df.iloc[current_index] # Create subplot with multiple views fig = make_subplots( rows=2, cols=2, subplot_titles=[ f'3D Track Progress - Frame {frame_num + 1}', 'Route Overview', 'Speed Over Time', 'Elevation Profile' ], specs=[[{'type': 'scene'}, {'type': 'scatter'}], [{'type': 'scatter'}, {'type': 'scatter'}]], vertical_spacing=0.1, horizontal_spacing=0.1 ) # 1. 3D Progressive Track View if len(current_track) > 1: # Show completed track in blue fig.add_trace( go.Scatter3d( x=current_track['longitude'], y=current_track['latitude'], z=current_track['elevation'], mode='lines+markers', line=dict(color='blue', width=5), marker=dict( size=3, color=current_track['speed_kmh'], colorscale='Viridis', showscale=True, colorbar=dict(title="Speed (km/h)", x=0.45) ), name='Completed Track', showlegend=False ), row=1, col=1 ) # Current vehicle position (moving marker) fig.add_trace( go.Scatter3d( x=[current_row['longitude']], y=[current_row['latitude']], z=[current_row['elevation'] + 15], mode='markers', marker=dict( size=15, color='red', symbol='diamond', line=dict(color='white', width=2) ), name=f'Vehicle - {current_row["timestamp"].strftime("%H:%M:%S")}', showlegend=False ), row=1, col=1 ) # 2. Top View - Route Overview with full track # Show full route in light gray fig.add_trace( go.Scatter( x=df['longitude'], y=df['latitude'], mode='lines', line=dict(color='lightgray', width=2, dash='dot'), name='Full Route', showlegend=False ), row=1, col=2 ) # Show completed track in color if len(current_track) > 1: fig.add_trace( go.Scatter( x=current_track['longitude'], y=current_track['latitude'], mode='lines+markers', line=dict(color='blue', width=4), marker=dict( size=4, color=current_track['speed_kmh'], colorscale='Viridis' ), name='Progress', showlegend=False ), row=1, col=2 ) # Current position on map fig.add_trace( go.Scatter( x=[current_row['longitude']], y=[current_row['latitude']], mode='markers', marker=dict( size=12, color='red', symbol='circle', line=dict(color='white', width=3) ), name='Current Position', showlegend=False ), row=1, col=2 ) # 3. Speed Profile Over Time if len(current_track) > 1: time_points = list(range(len(current_track))) fig.add_trace( go.Scatter( x=time_points, y=current_track['speed_kmh'], mode='lines+markers', line=dict(color='green', width=3), marker=dict(size=4), fill='tonexty', name='Speed', showlegend=False ), row=2, col=1 ) # Current speed marker fig.add_trace( go.Scatter( x=[current_index], y=[current_row['speed_kmh']], mode='markers', marker=dict(size=10, color='red'), name='Current Speed', showlegend=False ), row=2, col=1 ) # 4. Elevation Profile Over Time if len(current_track) > 1: time_points = list(range(len(current_track))) fig.add_trace( go.Scatter( x=time_points, y=current_track['elevation'], mode='lines+markers', line=dict(color='brown', width=3), marker=dict(size=4), fill='tonexty', name='Elevation', showlegend=False ), row=2, col=2 ) # Current elevation marker fig.add_trace( go.Scatter( x=[current_index], y=[current_row['elevation']], mode='markers', marker=dict(size=10, color='red'), name='Current Elevation', showlegend=False ), row=2, col=2 ) # Enhanced layout with better styling fig.update_layout( title=dict( text=f'GPS Track Animation - {current_row["timestamp"].strftime("%Y-%m-%d %H:%M:%S")}
' + f'Progress: {current_index + 1}/{len(df)} points ({((current_index + 1)/len(df)*100):.1f}%)', x=0.5, font=dict(size=16) ), showlegend=False, width=1920, height=1080, paper_bgcolor='white', plot_bgcolor='white' ) # Update 3D scene fig.update_scenes( camera=dict( eye=dict(x=1.5, y=1.5, z=1.2), center=dict(x=0, y=0, z=0), up=dict(x=0, y=0, z=1) ), aspectmode='cube' ) # Update axes labels fig.update_xaxes(title_text="Longitude", row=1, col=2) fig.update_yaxes(title_text="Latitude", row=1, col=2) fig.update_xaxes(title_text="Time Points", row=2, col=1) fig.update_yaxes(title_text="Speed (km/h)", row=2, col=1) fig.update_xaxes(title_text="Time Points", row=2, col=2) fig.update_yaxes(title_text="Elevation (m)", row=2, col=2) # Save frame frame_path = os.path.join(self.frames_folder, f"advanced_3d_frame_{frame_num:04d}.png") try: fig.write_image(frame_path, engine="kaleido", width=1920, height=1080) return frame_path except Exception as e: print(f"Error creating frame {frame_num}: {e}") return None def generate_frames(self, positions_file, style='advanced', progress_callback=None): """Generate Relive-style progressive animation frames""" print("Loading GPS data...") df = self.load_gps_data(positions_file) if len(df) < 2: print("Not enough GPS points for animation") return [] # Animation settings for smooth progression min_frames = 60 # Minimum frames for very short trips max_frames = 300 # Maximum frames to keep file size reasonable # Calculate optimal frame count based on trip length total_frames = min(max_frames, max(min_frames, len(df) * 2)) # Calculate step size for smooth progression step_size = len(df) / total_frames frame_paths = [] print(f"Generating {total_frames} frames for Relive-style animation...") print(f"Processing {len(df)} GPS points with step size {step_size:.2f}") for frame_num in range(total_frames): # Calculate which GPS point to show up to current_index = min(int(frame_num * step_size), len(df) - 1) # Ensure we always progress forward if current_index == 0: current_index = 1 try: if style == 'pydeck': frame_path = self.create_pydeck_frame(df, current_index, frame_num) elif style == 'plotly': frame_path = self.create_plotly_frame(df, current_index, frame_num) elif style == 'google_earth': frame_path = self.create_google_earth_frame(df, current_index, frame_num) else: # advanced (default) frame_path = self.create_advanced_plotly_frame(df, current_index, frame_num) if frame_path: frame_paths.append(frame_path) # Update progress if progress_callback: progress = ((frame_num + 1) / total_frames) * 100 progress_callback( progress, f"Creating animation frame {frame_num + 1}/{total_frames} (GPS point {current_index + 1}/{len(df)})" ) # Progress feedback if (frame_num + 1) % 20 == 0: print(f"Generated {frame_num + 1}/{total_frames} frames ({progress:.1f}%)") except Exception as e: print(f"Error generating frame {frame_num}: {e}") continue print(f"Successfully generated {len(frame_paths)} animation frames") return frame_paths def create_video(self, frame_paths, output_video_path, progress_callback=None): """Create video from frames using OpenCV for better compatibility""" print("Creating navigation animation video...") if not frame_paths: print("No frames to create video from") return False try: import cv2 # Filter out None paths valid_frames = [f for f in frame_paths if f and os.path.exists(f)] if not valid_frames: print("No valid frames found") return False print(f"Creating video from {len(valid_frames)} frames at {self.fps} FPS...") # Update progress if progress_callback: progress_callback(30, "Reading frame dimensions...") # Read first frame to get dimensions first_frame = cv2.imread(valid_frames[0]) if first_frame is None: print("Error reading first frame") return False height, width, layers = first_frame.shape # Update progress if progress_callback: progress_callback(40, "Setting up video encoder...") # Create video writer with OpenCV fourcc = cv2.VideoWriter_fourcc(*'mp4v') # You can also try 'XVID' video_writer = cv2.VideoWriter(output_video_path, fourcc, self.fps, (width, height)) if not video_writer.isOpened(): print("Error: Could not open video writer") return False # Update progress if progress_callback: progress_callback(50, "Writing frames to video...") # Write frames to video total_frames = len(valid_frames) for i, frame_path in enumerate(valid_frames): frame = cv2.imread(frame_path) if frame is not None: video_writer.write(frame) # Update progress periodically if progress_callback and i % 10 == 0: progress_percent = 50 + (i / total_frames) * 40 # 50-90% progress_callback(progress_percent, f"Writing frame {i+1}/{total_frames}...") # Clean up video_writer.release() cv2.destroyAllWindows() if progress_callback: progress_callback(100, f"Video successfully created: {os.path.basename(output_video_path)}") # Verify the video was created if os.path.exists(output_video_path) and os.path.getsize(output_video_path) > 0: print(f"✅ Navigation animation video saved to: {output_video_path}") file_size = os.path.getsize(output_video_path) / (1024 * 1024) # MB print(f"📊 Video info: {len(valid_frames)} frames, {self.fps} FPS, {file_size:.1f} MB") return True else: print("Error: Video file was not created properly") return False except Exception as e: print(f"Error creating video: {e}") if progress_callback: progress_callback(-1, f"Error: {e}") return False def cleanup_frames(self): """Clean up temporary frame files""" import shutil if os.path.exists(self.frames_folder): shutil.rmtree(self.frames_folder) os.makedirs(self.frames_folder, exist_ok=True) def create_google_earth_frame(self, df, current_index, frame_num): """ Create a Google Earth-style flythrough frame with realistic terrain and camera following """ if not PLOTLY_AVAILABLE: raise ImportError("Plotly is required for Google Earth-style frames") # Get current position and context current_row = df.iloc[current_index] current_lat = current_row['latitude'] current_lon = current_row['longitude'] current_alt = current_row.get('elevation', 100) # Get track up to current position (progressive reveal) track_so_far = df.iloc[:current_index + 1] # Calculate terrain bounds around the track lat_margin = 0.02 # degrees lon_margin = 0.02 # degrees min_lat = track_so_far['latitude'].min() - lat_margin max_lat = track_so_far['latitude'].max() + lat_margin min_lon = track_so_far['longitude'].min() - lon_margin max_lon = track_so_far['longitude'].max() + lon_margin # Generate terrain mesh terrain_resolution = 40 lat_range = np.linspace(min_lat, max_lat, terrain_resolution) lon_range = np.linspace(min_lon, max_lon, terrain_resolution) lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range) # Generate realistic terrain heights terrain_heights = self.generate_terrain_heights(lat_mesh, lon_mesh, current_lat, current_lon) # Create the figure fig = go.Figure() # Add terrain surface fig.add_trace( go.Surface( x=lon_mesh, y=lat_mesh, z=terrain_heights, colorscale=[ [0.0, 'rgb(139,69,19)'], # Brown (low elevation) [0.2, 'rgb(160,82,45)'], # Saddle brown [0.4, 'rgb(107,142,35)'], # Olive drab (medium) [0.6, 'rgb(34,139,34)'], # Forest green [0.8, 'rgb(105,105,105)'], # Dim gray (high) [1.0, 'rgb(255,255,255)'] # White (peaks) ], opacity=0.9, showscale=False, name='Terrain', lighting=dict( ambient=0.3, diffuse=0.8, specular=0.3, roughness=0.3 ) ) ) # Add GPS track so far (elevated above terrain) if len(track_so_far) > 1: track_elevation = track_so_far['elevation'].values + 80 # 80m above terrain fig.add_trace( go.Scatter3d( x=track_so_far['longitude'], y=track_so_far['latitude'], z=track_elevation, mode='lines+markers', line=dict( color='red', width=10 ), marker=dict( size=4, color='orange', opacity=0.8 ), name='GPS Track' ) ) # Add current vehicle position vehicle_height = current_alt + 120 # Above terrain fig.add_trace( go.Scatter3d( x=[current_lon], y=[current_lat], z=[vehicle_height], mode='markers', marker=dict( color='red', size=20, symbol='diamond', line=dict(color='yellow', width=3) ), name='Vehicle' ) ) # Calculate dynamic camera position for cinematic following # Camera follows behind and above the vehicle follow_distance = 0.005 # degrees behind camera_height_offset = 1000 # meters above vehicle if current_index > 5: # Calculate movement direction from last few points recent_track = df.iloc[max(0, current_index-5):current_index+1] lat_direction = recent_track['latitude'].iloc[-1] - recent_track['latitude'].iloc[0] lon_direction = recent_track['longitude'].iloc[-1] - recent_track['longitude'].iloc[0] # Normalize direction direction_length = np.sqrt(lat_direction**2 + lon_direction**2) if direction_length > 0: lat_direction /= direction_length lon_direction /= direction_length # Position camera behind the vehicle camera_lat = current_lat - lat_direction * follow_distance camera_lon = current_lon - lon_direction * follow_distance else: camera_lat = current_lat - follow_distance camera_lon = current_lon - follow_distance camera_z = (current_alt + camera_height_offset) / 1000 # Convert to relative scale # Update layout for cinematic Google Earth-style view fig.update_layout( title=dict( text=f'GPS Flythrough - {current_row["timestamp"].strftime("%H:%M:%S")} - Frame {frame_num}', x=0.5, font=dict(size=24, color='white', family="Arial Black") ), scene=dict( camera=dict( eye=dict( x=1.2, # Camera position relative to scene y=-1.5, z=0.8 ), center=dict( x=0, y=0, z=0.2 ), up=dict(x=0, y=0, z=1) ), xaxis=dict( title='', showgrid=False, zeroline=False, showline=False, showticklabels=False, showbackground=False ), yaxis=dict( title='', showgrid=False, zeroline=False, showline=False, showticklabels=False, showbackground=False ), zaxis=dict( title='', showgrid=False, zeroline=False, showline=False, showticklabels=False, showbackground=False ), aspectmode='cube', bgcolor='rgb(135,206,235)', # Sky blue background camera_projection_type='perspective' ), paper_bgcolor='rgb(0,0,0)', plot_bgcolor='rgb(0,0,0)', showlegend=False, width=1920, height=1080, margin=dict(l=0, r=0, t=60, b=0), font=dict(color='white') ) # Save frame frame_path = os.path.join(self.frames_folder, f"GoogleEarth_Frame_{frame_num:04d}.png") try: fig.write_image(frame_path, engine="kaleido", width=1920, height=1080) return frame_path except Exception as e: print(f"Error saving frame {frame_num}: {e}") return None def generate_terrain_heights(self, lat_mesh, lon_mesh, center_lat, center_lon): """ Generate realistic terrain heights using mathematical functions to simulate mountains/hills """ # Convert lat/lon to local coordinates (approximate) lat_m = (lat_mesh - center_lat) * 111000 # degrees to meters lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat)) # Base terrain height base_height = 300 # Create mountain ridges using sine waves ridge1 = 400 * np.exp(-((lat_m - 1000)**2 + (lon_m + 500)**2) / (800**2)) ridge2 = 500 * np.exp(-((lat_m + 800)**2 + (lon_m - 1200)**2) / (1000**2)) ridge3 = 350 * np.exp(-((lat_m)**2 + (lon_m)**2) / (1500**2)) # Add rolling hills hills = 150 * np.sin(lat_m / 400) * np.cos(lon_m / 600) hills += 100 * np.sin(lat_m / 800) * np.sin(lon_m / 300) # Add valleys valleys = -80 * np.exp(-((lat_m - 500)**2 + (lon_m + 800)**2) / (600**2)) # Combine all terrain features terrain = base_height + ridge1 + ridge2 + ridge3 + hills + valleys # Add some noise for realism noise = 30 * np.sin(lat_m / 100) * np.cos(lon_m / 150) terrain += noise # Ensure minimum elevation terrain = np.maximum(terrain, 50) return terrain