diff --git a/py_scripts/advanced_3d_generator.py b/py_scripts/advanced_3d_generator.py index 38c3122..f8e9eca 100644 --- a/py_scripts/advanced_3d_generator.py +++ b/py_scripts/advanced_3d_generator.py @@ -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): """ diff --git a/screens/create_animation_screen.py b/screens/create_animation_screen.py index a443c0c..b3061a3 100644 --- a/screens/create_animation_screen.py +++ b/screens/create_animation_screen.py @@ -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) diff --git a/test_google_earth.py b/test_google_earth.py new file mode 100644 index 0000000..565a1ad --- /dev/null +++ b/test_google_earth.py @@ -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()