updated v
This commit is contained in:
@@ -529,7 +529,9 @@ class Advanced3DGenerator:
|
||||
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)
|
||||
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)
|
||||
|
||||
if frame_path:
|
||||
@@ -654,6 +656,237 @@ class Advanced3DGenerator:
|
||||
progress_callback(-1, f"Error: {e}")
|
||||
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",
|
||||
style='advanced', progress_callback=None):
|
||||
"""
|
||||
|
||||
@@ -331,6 +331,39 @@ class CreateAnimationScreen(Screen):
|
||||
)
|
||||
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_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||
blender_title = Label(
|
||||
@@ -393,6 +426,10 @@ class CreateAnimationScreen(Screen):
|
||||
popup.dismiss()
|
||||
self.generate_advanced_3d_animation()
|
||||
|
||||
def start_google_earth(instance):
|
||||
popup.dismiss()
|
||||
self.generate_google_earth_animation()
|
||||
|
||||
def start_blender_animation(instance):
|
||||
popup.dismiss()
|
||||
self.generate_blender_animation()
|
||||
@@ -400,6 +437,7 @@ class CreateAnimationScreen(Screen):
|
||||
classic_test_btn.bind(on_press=start_classic_test)
|
||||
classic_prod_btn.bind(on_press=start_classic_production)
|
||||
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)
|
||||
cancel_btn.bind(on_press=lambda x: popup.dismiss())
|
||||
|
||||
@@ -573,6 +611,91 @@ class CreateAnimationScreen(Screen):
|
||||
# Schedule the animation generation
|
||||
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):
|
||||
"""Show success popup with option to open file location"""
|
||||
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