Files
traccar_animation/py_scripts/advanced_3d_generator.py
2025-07-10 13:46:05 +03:00

1233 lines
45 KiB
Python

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<br>"
speed_text += f"Time: {current_row['timestamp'].strftime('%H:%M:%S')}<br>"
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")}<br>' +
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