updated v
This commit is contained in:
@@ -529,7 +529,9 @@ class Advanced3DGenerator:
|
|||||||
frame_path = self.create_pydeck_frame(df, current_index, frame_num)
|
frame_path = self.create_pydeck_frame(df, current_index, frame_num)
|
||||||
elif style == 'plotly':
|
elif style == 'plotly':
|
||||||
frame_path = self.create_plotly_frame(df, current_index, frame_num)
|
frame_path = self.create_plotly_frame(df, current_index, frame_num)
|
||||||
else: # advanced
|
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)
|
frame_path = self.create_advanced_plotly_frame(df, current_index, frame_num)
|
||||||
|
|
||||||
if frame_path:
|
if frame_path:
|
||||||
@@ -654,6 +656,237 @@ class Advanced3DGenerator:
|
|||||||
progress_callback(-1, f"Error: {e}")
|
progress_callback(-1, f"Error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def generate_advanced_3d_video(positions_file, output_folder, filename_prefix="advanced_3d",
|
def generate_advanced_3d_video(positions_file, output_folder, filename_prefix="advanced_3d",
|
||||||
style='advanced', progress_callback=None):
|
style='advanced', progress_callback=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -331,6 +331,39 @@ class CreateAnimationScreen(Screen):
|
|||||||
)
|
)
|
||||||
layout.add_widget(advanced_btn)
|
layout.add_widget(advanced_btn)
|
||||||
|
|
||||||
|
# Google Earth Flythrough Mode
|
||||||
|
google_earth_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||||
|
google_earth_title = Label(
|
||||||
|
text="🌍 Google Earth Flythrough",
|
||||||
|
font_size=16,
|
||||||
|
size_hint_y=None,
|
||||||
|
height=30,
|
||||||
|
color=(0.1, 0.8, 0.1, 1)
|
||||||
|
)
|
||||||
|
google_earth_desc = Label(
|
||||||
|
text="• Realistic 3D terrain with mountains\n• Cinematic camera following at 1000-2000m\n• Google Earth-style flythrough\n• Professional geographic animation",
|
||||||
|
font_size=11,
|
||||||
|
size_hint_y=None,
|
||||||
|
height=70,
|
||||||
|
color=(0.9, 0.9, 0.9, 1),
|
||||||
|
halign="left",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
google_earth_desc.text_size = (None, None)
|
||||||
|
google_earth_layout.add_widget(google_earth_title)
|
||||||
|
google_earth_layout.add_widget(google_earth_desc)
|
||||||
|
layout.add_widget(google_earth_layout)
|
||||||
|
|
||||||
|
# Google Earth button
|
||||||
|
google_earth_btn = Button(
|
||||||
|
text="Generate Google Earth Flythrough",
|
||||||
|
background_color=(0.1, 0.8, 0.1, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=45,
|
||||||
|
font_size=13
|
||||||
|
)
|
||||||
|
layout.add_widget(google_earth_btn)
|
||||||
|
|
||||||
# Blender Cinema Mode
|
# Blender Cinema Mode
|
||||||
blender_layout = BoxLayout(orientation='vertical', spacing=5)
|
blender_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||||
blender_title = Label(
|
blender_title = Label(
|
||||||
@@ -393,6 +426,10 @@ class CreateAnimationScreen(Screen):
|
|||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.generate_advanced_3d_animation()
|
self.generate_advanced_3d_animation()
|
||||||
|
|
||||||
|
def start_google_earth(instance):
|
||||||
|
popup.dismiss()
|
||||||
|
self.generate_google_earth_animation()
|
||||||
|
|
||||||
def start_blender_animation(instance):
|
def start_blender_animation(instance):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.generate_blender_animation()
|
self.generate_blender_animation()
|
||||||
@@ -400,6 +437,7 @@ class CreateAnimationScreen(Screen):
|
|||||||
classic_test_btn.bind(on_press=start_classic_test)
|
classic_test_btn.bind(on_press=start_classic_test)
|
||||||
classic_prod_btn.bind(on_press=start_classic_production)
|
classic_prod_btn.bind(on_press=start_classic_production)
|
||||||
advanced_btn.bind(on_press=start_advanced_3d)
|
advanced_btn.bind(on_press=start_advanced_3d)
|
||||||
|
google_earth_btn.bind(on_press=start_google_earth)
|
||||||
blender_btn.bind(on_press=start_blender_animation)
|
blender_btn.bind(on_press=start_blender_animation)
|
||||||
cancel_btn.bind(on_press=lambda x: popup.dismiss())
|
cancel_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
|
||||||
@@ -573,6 +611,91 @@ class CreateAnimationScreen(Screen):
|
|||||||
# Schedule the animation generation
|
# Schedule the animation generation
|
||||||
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
||||||
|
|
||||||
|
def generate_google_earth_animation(self):
|
||||||
|
"""Generate Google Earth-style flythrough animation with terrain"""
|
||||||
|
# Show processing popup
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
label = Label(text="Initializing Google Earth flythrough...")
|
||||||
|
progress = ProgressBar(max=100, value=0)
|
||||||
|
layout.add_widget(label)
|
||||||
|
layout.add_widget(progress)
|
||||||
|
popup = Popup(
|
||||||
|
title="Generating Google Earth Flythrough",
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.9, None),
|
||||||
|
size=(0, 200),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def run_google_earth_animation():
|
||||||
|
try:
|
||||||
|
# Update status
|
||||||
|
def update_status(progress_val, status_text):
|
||||||
|
def _update(dt):
|
||||||
|
progress.value = progress_val
|
||||||
|
label.text = status_text
|
||||||
|
Clock.schedule_once(_update, 0)
|
||||||
|
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||||
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
update_status(0, "Error: No GPS data found")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(10, "Checking dependencies...")
|
||||||
|
|
||||||
|
# Check dependencies first
|
||||||
|
generator = Advanced3DGenerator(project_folder)
|
||||||
|
generator.check_dependencies()
|
||||||
|
|
||||||
|
update_status(20, "Loading GPS data...")
|
||||||
|
df = generator.load_gps_data(positions_path)
|
||||||
|
|
||||||
|
update_status(30, "Generating terrain and camera flythrough...")
|
||||||
|
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Progress callback for the generator
|
||||||
|
def generator_progress(progress, message):
|
||||||
|
update_status(30 + (progress * 0.5), message) # Map 0-100% to 30-80%
|
||||||
|
|
||||||
|
update_status(80, "Creating flythrough video...")
|
||||||
|
success = generator.generate_3d_animation(
|
||||||
|
positions_path,
|
||||||
|
output_video_path,
|
||||||
|
style='google_earth',
|
||||||
|
progress_callback=generator_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
update_status(100, "Google Earth flythrough complete!")
|
||||||
|
output_path = output_video_path
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to generate flythrough video")
|
||||||
|
|
||||||
|
def show_success(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_success_popup(
|
||||||
|
"Google Earth Flythrough Complete!",
|
||||||
|
f"Your cinematic flythrough has been created:\n{output_path}",
|
||||||
|
output_path
|
||||||
|
)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_success, 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
def show_error(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_error_popup("Google Earth Animation Error", error_message)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
|
# Schedule the animation generation
|
||||||
|
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
|
||||||
|
|
||||||
def show_success_popup(self, title, message, file_path=None):
|
def show_success_popup(self, title, message, file_path=None):
|
||||||
"""Show success popup with option to open file location"""
|
"""Show success popup with option to open file location"""
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
|||||||
83
test_google_earth.py
Normal file
83
test_google_earth.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for Google Earth-style flythrough animation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
|
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def test_google_earth_animation():
|
||||||
|
"""Test the new Google Earth flythrough animation"""
|
||||||
|
|
||||||
|
# Find a project with GPS data
|
||||||
|
projects_folder = "/home/pi/Desktop/traccar_animation/resources/projects"
|
||||||
|
|
||||||
|
if not os.path.exists(projects_folder):
|
||||||
|
print("Projects folder not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Look for projects
|
||||||
|
projects = [d for d in os.listdir(projects_folder) if os.path.isdir(os.path.join(projects_folder, d))]
|
||||||
|
|
||||||
|
if not projects:
|
||||||
|
print("No projects found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use the first project found
|
||||||
|
project_name = projects[0]
|
||||||
|
project_folder = os.path.join(projects_folder, project_name)
|
||||||
|
positions_file = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_file):
|
||||||
|
print(f"No positions.json found in project {project_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Testing Google Earth animation with project: {project_name}")
|
||||||
|
|
||||||
|
# Create generator
|
||||||
|
generator = Advanced3DGenerator(project_folder)
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
try:
|
||||||
|
generator.check_dependencies()
|
||||||
|
print("✅ All dependencies available")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Dependency error: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate Google Earth-style animation
|
||||||
|
output_video = os.path.join(project_folder, f"{project_name}_google_earth_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
def progress_callback(progress, message):
|
||||||
|
print(f"Progress: {progress:.1f}% - {message}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Starting Google Earth flythrough generation...")
|
||||||
|
success = generator.generate_3d_animation(
|
||||||
|
positions_file,
|
||||||
|
output_video,
|
||||||
|
style='google_earth',
|
||||||
|
progress_callback=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and os.path.exists(output_video):
|
||||||
|
print(f"✅ SUCCESS! Google Earth flythrough created: {output_video}")
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
|
||||||
|
print(f"📹 Video size: {file_size:.1f} MB")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create video")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error during generation: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_google_earth_animation()
|
||||||
Reference in New Issue
Block a user