import kivy from kivy.uix.screenmanager import Screen import os import json import math from datetime import datetime from kivy.clock import Clock from kivy.properties import StringProperty, NumericProperty, AliasProperty from py_scripts.utils import ( process_preview_util, optimize_route_entries_util ) from py_scripts.advanced_3d_generator import NavigationAnimationGenerator # BlenderGPSAnimator imported conditionally when needed from kivy.uix.popup import Popup from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.boxlayout import BoxLayout from kivy.uix.progressbar import ProgressBar from kivy.uix.textinput import TextInput from config import RESOURCES_FOLDER class CreateAnimationScreen(Screen): project_name = StringProperty("") preview_html_path = StringProperty("") # Path to the HTML file for preview preview_image_path = StringProperty("") # Add this line preview_image_version = NumericProperty(0) # Add this line def get_preview_image_source(self): project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) img_path = os.path.join(project_folder, "preview.png") if os.path.exists(img_path): return img_path return "resources/images/track.png" preview_image_source = AliasProperty( get_preview_image_source, None, bind=['project_name', 'preview_image_version'] ) def on_pre_enter(self): # Update the route entries label with the actual number of entries project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) positions_path = os.path.join(project_folder, "positions.json") count = 0 if os.path.exists(positions_path): with open(positions_path, "r") as f: try: positions = json.load(f) count = len(positions) except Exception: count = 0 self.ids.route_entries_label.text = f"Your route has {count} entries," def open_rename_popup(self): from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.textinput import TextInput from kivy.uix.label import Label layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Enter new project name:") input_field = TextInput(text=self.project_name, multiline=False) btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1)) btn_cancel = Button(text="Cancel") layout.add_widget(label) layout.add_widget(input_field) layout.add_widget(btn_save) layout.add_widget(btn_cancel) popup = Popup( title="Rename Project", content=layout, size_hint=(0.92, None), size=(0, 260), auto_dismiss=False ) def do_rename(instance): new_name = input_field.text.strip() if new_name and new_name != self.project_name: if self.rename_project_folder(self.project_name, new_name): self.project_name = new_name popup.dismiss() self.on_pre_enter() # Refresh label else: label.text = "Rename failed (name exists?)" else: label.text = "Please enter a new name." btn_save.bind(on_press=do_rename) btn_cancel.bind(on_press=lambda x: popup.dismiss()) popup.open() def rename_project_folder(self, old_name, new_name): import os old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name) new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name) if os.path.exists(old_path) and not os.path.exists(new_path): os.rename(old_path, new_path) return True return False def optimize_route_entries(self): # Create the popup and UI elements layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Processing route entries...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Optimizing Route", content=layout, size_hint=(0.92, None), size=(0, 260), auto_dismiss=False ) popup.open() # Now call the utility function with these objects optimize_route_entries_util( self.project_name, RESOURCES_FOLDER, label, progress, popup, Clock, on_save=lambda: self.on_pre_enter() ) def preview_route(self): # Show processing popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Processing route preview...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Previewing Route", content=layout, size_hint=(0.8, None), size=(0, 180), auto_dismiss=False ) popup.open() def set_preview_image_path(path): self.preview_image_path = path self.preview_image_version += 1 # Force AliasProperty to update self.property('preview_image_source').dispatch(self) self.ids.preview_image.reload() # Schedule the processing function Clock.schedule_once( lambda dt: process_preview_util( self.project_name, RESOURCES_FOLDER, label, progress, popup, self.ids.preview_image, set_preview_image_path, Clock ), 0.5 ) def generate_google_earth_animation(self): """Generate Google Earth-style flythrough animation using NavigationAnimationGenerator""" # 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, "Loading GPS data...") # Check dependencies first generator = NavigationAnimationGenerator(project_folder) generator.check_dependencies() update_status(20, "Processing GPS coordinates...") df = generator.load_gps_data(positions_path) update_status(40, "Creating Google Earth 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(40 + (progress * 0.5), message) # Map 0-100% to 40-90% update_status(90, "Creating flythrough video...") success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress) if success and len(success) > 0: update_status(95, "Rendering final video...") video_success = generator.create_video(success, output_video_path, generator_progress) if video_success: update_status(100, "Google Earth flythrough complete!") output_path = output_video_path else: raise Exception("Failed to create video from frames") else: raise Exception("Failed to generate frames") def show_success(dt): popup.dismiss() self.show_success_popup( "Google Earth Flythrough Complete!", f"Your Google Earth-style flythrough has been saved to:\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 generate_blender_animation(self): """Generate cinema-quality animation using Blender (or fallback to advanced 3D)""" # Show processing popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Initializing cinema rendering pipeline...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Generating Cinema Animation", content=layout, size_hint=(0.9, None), size=(0, 200), auto_dismiss=False ) popup.open() def run_blender_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 # Check if Blender is available try: from py_scripts.blender_animator import BLENDER_AVAILABLE, BlenderGPSAnimator if BLENDER_AVAILABLE: update_status(10, "Loading GPS data into Blender...") # Use Blender for rendering animator = BlenderGPSAnimator(project_folder) animator.check_dependencies() update_status(25, "Processing GPS coordinates...") gps_data = animator.load_gps_data(positions_path) output_video_path = os.path.join(project_folder, f"{self.project_name}_blender_cinema_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4") # Progress callback for the animator def animator_progress(progress, message): update_status(25 + (progress * 0.6), message) # Map 0-100% to 25-85% update_status(85, "Rendering cinema-quality video...") success = animator.create_gps_animation( positions_path, output_video_path, progress_callback=animator_progress ) if success: update_status(100, "Blender cinema animation complete!") output_path = output_video_path else: raise Exception("Failed to generate Blender animation") else: raise ImportError("Blender not available") except ImportError: # Fallback to advanced 3D animation with cinema-quality settings update_status(10, "Blender not available - using advanced 3D cinema mode...") # Import here to avoid startup delays import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np import cv2 # Load GPS data with open(positions_path, 'r') as f: positions = json.load(f) if len(positions) < 2: update_status(0, "Error: Need at least 2 GPS points") Clock.schedule_once(lambda dt: popup.dismiss(), 2) return update_status(20, "Processing GPS coordinates for cinema rendering...") # Extract coordinates lats = np.array([pos['latitude'] for pos in positions]) lons = np.array([pos['longitude'] for pos in positions]) alts = np.array([pos.get('altitude', 0) for pos in positions]) timestamps = [pos.get('fixTime', '') for pos in positions] # Convert to relative coordinates lat_center = np.mean(lats) lon_center = np.mean(lons) alt_min = np.min(alts) x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center)) y = (lats - lat_center) * 110540 z = alts - alt_min update_status(30, "Creating cinema-quality frames...") # Cinema settings - higher quality frames_folder = os.path.join(project_folder, "cinema_frames") os.makedirs(frames_folder, exist_ok=True) fps = 24 # Cinema standard total_frames = min(len(positions), 200) # Limit for reasonable processing time points_per_frame = max(1, len(positions) // total_frames) frame_files = [] # Generate cinema-quality frames for frame_idx in range(total_frames): current_progress = 30 + (frame_idx / total_frames) * 50 update_status(current_progress, f"Rendering cinema frame {frame_idx + 1}/{total_frames}...") end_point = min((frame_idx + 1) * points_per_frame, len(positions)) # Create high-quality 3D plot plt.style.use('dark_background') # Cinema-style dark theme fig = plt.figure(figsize=(16, 12), dpi=150) # Higher resolution ax = fig.add_subplot(111, projection='3d') # Plot route with cinema styling if end_point > 1: # Gradient effect for completed route colors = np.linspace(0, 1, end_point) ax.scatter(x[:end_point], y[:end_point], z[:end_point], c=colors, cmap='plasma', s=30, alpha=0.8) ax.plot(x[:end_point], y[:end_point], z[:end_point], color='cyan', linewidth=3, alpha=0.9) # Current position with glow effect if end_point > 0: current_idx = end_point - 1 # Multiple layers for glow effect for size, alpha in [(200, 0.3), (150, 0.5), (100, 0.8)]: ax.scatter(x[current_idx], y[current_idx], z[current_idx], c='yellow', s=size, alpha=alpha, marker='o') # Trail effect trail_start = max(0, current_idx - 10) if current_idx > trail_start: trail_alpha = np.linspace(0.3, 1.0, current_idx - trail_start + 1) for i, alpha in enumerate(trail_alpha): idx = trail_start + i ax.scatter(x[idx], y[idx], z[idx], c='orange', s=60, alpha=alpha) # Remaining route preview if end_point < len(positions): ax.plot(x[end_point:], y[end_point:], z[end_point:], color='gray', linewidth=1, alpha=0.4, linestyle='--') # Cinema-style labels and styling ax.set_xlabel('East-West (m)', color='white', fontsize=14) ax.set_ylabel('North-South (m)', color='white', fontsize=14) ax.set_zlabel('Elevation (m)', color='white', fontsize=14) # Progress and time info progress_percent = (end_point / len(positions)) * 100 timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start" ax.set_title(f'CINEMA GPS JOURNEY\nProgress: {progress_percent:.1f}% • Point {end_point}/{len(positions)} • {timestamp_str}', color='white', fontsize=16, pad=20, weight='bold') # Consistent view with cinematic angle margin = max(np.ptp(x), np.ptp(y)) * 0.15 ax.set_xlim(np.min(x) - margin, np.max(x) + margin) ax.set_ylim(np.min(y) - margin, np.max(y) + margin) ax.set_zlim(np.min(z) - 20, np.max(z) + 20) # Dynamic camera movement for cinematic effect azim = 45 + (frame_idx * 0.5) % 360 # Slowly rotating view ax.view_init(elev=25, azim=azim) # Cinema-style grid ax.grid(True, alpha=0.2, color='white') ax.xaxis.pane.fill = False ax.yaxis.pane.fill = False ax.zaxis.pane.fill = False # Make pane edges more subtle ax.xaxis.pane.set_edgecolor('gray') ax.yaxis.pane.set_edgecolor('gray') ax.zaxis.pane.set_edgecolor('gray') ax.xaxis.pane.set_alpha(0.1) ax.yaxis.pane.set_alpha(0.1) ax.zaxis.pane.set_alpha(0.1) # Save high-quality frame frame_path = os.path.join(frames_folder, f"cinema_frame_{frame_idx:04d}.png") try: plt.savefig(frame_path, dpi=150, bbox_inches='tight', facecolor='black', edgecolor='none', format='png') plt.close(fig) if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024: test_frame = cv2.imread(frame_path) if test_frame is not None: frame_files.append(frame_path) if frame_idx == 0: h, w, c = test_frame.shape update_status(current_progress, f"Cinema quality: {w}x{h} at {fps} FPS") except Exception as frame_error: update_status(current_progress, f"Error creating frame {frame_idx}: {str(frame_error)}") plt.close(fig) continue plt.style.use('default') # Reset style # Create cinema video if not frame_files: raise Exception("No valid cinema frames were generated") update_status(80, f"Creating cinema video from {len(frame_files)} frames...") output_video_path = os.path.join(project_folder, f"{self.project_name}_cinema_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4") # Cinema video creation with higher quality first_frame = cv2.imread(frame_files[0]) height, width, layers = first_frame.shape # Try to create high-quality video fourcc = cv2.VideoWriter_fourcc(*'mp4v') video_writer = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height)) if video_writer.isOpened(): for i, frame_file in enumerate(frame_files): frame = cv2.imread(frame_file) if frame is not None: video_writer.write(frame) if i % 10 == 0: progress = 80 + (i / len(frame_files)) * 8 update_status(progress, f"Encoding cinema frame {i+1}/{len(frame_files)}") video_writer.release() if os.path.exists(output_video_path) and os.path.getsize(output_video_path) > 1024: update_status(90, "Cinema video created successfully") output_path = output_video_path else: raise Exception("Cinema video creation failed") else: raise Exception("Could not initialize cinema video writer") # Clean up frames for frame_file in frame_files: try: os.remove(frame_file) except: pass try: os.rmdir(frames_folder) except: pass update_status(100, "Cinema animation complete!") def show_success(dt): popup.dismiss() self.show_success_popup( "Cinema Animation Complete!", f"Your cinema-quality animation has been saved to:\n{output_path}\n\nNote: Blender was not available, so advanced 3D cinema mode was used instead.", output_path ) Clock.schedule_once(show_success, 1) except Exception as e: error_message = str(e) print(f"DEBUG: Cinema animation error: {error_message}") import traceback traceback.print_exc() def show_error(dt): popup.dismiss() self.show_error_popup("Cinema Animation Error", error_message) Clock.schedule_once(show_error, 0) # Schedule the animation generation Clock.schedule_once(lambda dt: run_blender_animation(), 0.5) def generate_progressive_3d_animation(self): """Generate a progressive 3D animation that builds the trip point by point""" # Show processing popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Initializing progressive 3D animation...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Generating Progressive 3D Animation", content=layout, size_hint=(0.9, None), size=(0, 200), auto_dismiss=False ) popup.open() def run_progressive_animation(): try: # Import here to avoid startup delays import matplotlib matplotlib.use('Agg') # Use non-interactive backend import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np import cv2 # Use OpenCV instead of MoviePy # 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, "Loading GPS data...") # Load GPS data with open(positions_path, 'r') as f: positions = json.load(f) if len(positions) < 2: update_status(0, "Error: Need at least 2 GPS points") Clock.schedule_once(lambda dt: popup.dismiss(), 2) return update_status(20, "Processing GPS coordinates...") # Extract coordinates and timestamps lats = [pos['latitude'] for pos in positions] lons = [pos['longitude'] for pos in positions] alts = [pos.get('altitude', 0) for pos in positions] timestamps = [pos.get('fixTime', '') for pos in positions] # Convert to numpy arrays for easier manipulation lats = np.array(lats) lons = np.array(lons) alts = np.array(alts) # Normalize coordinates for better visualization lat_center = np.mean(lats) lon_center = np.mean(lons) alt_min = np.min(alts) # Convert to relative coordinates (in meters approximately) x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center)) # longitude to meters y = (lats - lat_center) * 110540 # latitude to meters z = alts - alt_min # relative altitude update_status(30, "Creating animation frames...") # Create frames folder frames_folder = os.path.join(project_folder, "progressive_frames") os.makedirs(frames_folder, exist_ok=True) # Animation settings fps = 10 # frames per second points_per_frame = max(1, len(positions) // 100) # Show multiple points per frame for long routes total_frames = len(positions) // points_per_frame frame_files = [] # Generate frames for frame_idx in range(total_frames): current_progress = 30 + (frame_idx / total_frames) * 50 update_status(current_progress, f"Creating frame {frame_idx + 1}/{total_frames}...") # Points to show in this frame end_point = min((frame_idx + 1) * points_per_frame, len(positions)) # Create 3D plot fig = plt.figure(figsize=(12, 9), dpi=100) ax = fig.add_subplot(111, projection='3d') # Plot the route progressively if end_point > 1: # Plot completed route in blue ax.plot(x[:end_point], y[:end_point], z[:end_point], 'b-', linewidth=2, alpha=0.7, label='Route') # Plot points as small dots ax.scatter(x[:end_point], y[:end_point], z[:end_point], c='blue', s=20, alpha=0.6) # Highlight current position in red if end_point > 0: current_idx = end_point - 1 ax.scatter(x[current_idx], y[current_idx], z[current_idx], c='red', s=100, marker='o', label='Current Position') # Add a small trail behind current position trail_start = max(0, current_idx - 5) if current_idx > trail_start: ax.plot(x[trail_start:current_idx+1], y[trail_start:current_idx+1], z[trail_start:current_idx+1], 'r-', linewidth=4, alpha=0.8) # Plot remaining route in light gray (preview) if end_point < len(positions): ax.plot(x[end_point:], y[end_point:], z[end_point:], 'lightgray', linewidth=1, alpha=0.3, label='Remaining Route') # Set labels and title ax.set_xlabel('East-West (meters)') ax.set_ylabel('North-South (meters)') ax.set_zlabel('Elevation (meters)') # Add progress info progress_percent = (end_point / len(positions)) * 100 timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start" ax.set_title(f'GPS Trip Animation\nProgress: {progress_percent:.1f}% - Point {end_point}/{len(positions)}\nTime: {timestamp_str}', fontsize=14, pad=20) # Set consistent view limits for all frames margin = max(np.ptp(x), np.ptp(y)) * 0.1 ax.set_xlim(np.min(x) - margin, np.max(x) + margin) ax.set_ylim(np.min(y) - margin, np.max(y) + margin) ax.set_zlim(np.min(z) - 10, np.max(z) + 10) # Set viewing angle for better 3D perspective ax.view_init(elev=20, azim=45) # Add legend ax.legend(loc='upper right') # Add grid ax.grid(True, alpha=0.3) # Save frame with comprehensive error handling frame_path = os.path.join(frames_folder, f"frame_{frame_idx:04d}.png") try: plt.savefig(frame_path, dpi=100, bbox_inches='tight', facecolor='white', edgecolor='none', format='png', optimize=False) plt.close(fig) # Verify frame was saved properly and is readable by OpenCV if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024: # Test if OpenCV can read the frame test_frame = cv2.imread(frame_path) if test_frame is not None: frame_files.append(frame_path) if frame_idx == 0: # Log first frame details h, w, c = test_frame.shape update_status(current_progress, f"First frame: {w}x{h}, size: {os.path.getsize(frame_path)} bytes") else: update_status(current_progress, f"Warning: Frame {frame_idx} not readable by OpenCV") try: os.remove(frame_path) except: pass else: update_status(current_progress, f"Warning: Frame {frame_idx} too small or missing") except Exception as e: update_status(current_progress, f"Error saving frame {frame_idx}: {str(e)}") try: plt.close(fig) except: pass continue # Validate frames before creating video if not frame_files: raise Exception("No valid frames were generated") update_status(80, f"Creating video from {len(frame_files)} frames...") # Create video using OpenCV with better error handling output_video_path = os.path.join(project_folder, f"{self.project_name}_progressive_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4") if frame_files: try: # Read first frame to get dimensions first_frame = cv2.imread(frame_files[0]) if first_frame is None: raise Exception(f"Could not read first frame: {frame_files[0]}") height, width, layers = first_frame.shape update_status(82, f"Video dimensions: {width}x{height}") # Try different codecs for better compatibility codecs_to_try = [ ('mp4v', '.mp4'), ('XVID', '.avi'), ('MJPG', '.avi') ] video_created = False for codec, ext in codecs_to_try: try: # Update output path for different codecs if ext != '.mp4': test_output_path = output_video_path.replace('.mp4', ext) else: test_output_path = output_video_path update_status(84, f"Trying codec {codec}...") # Create video writer fourcc = cv2.VideoWriter_fourcc(*codec) video_writer = cv2.VideoWriter(test_output_path, fourcc, fps, (width, height)) if not video_writer.isOpened(): update_status(85, f"Failed to open video writer with {codec}") continue # Add frames to video frames_written = 0 for i, frame_file in enumerate(frame_files): frame = cv2.imread(frame_file) if frame is not None: # Ensure frame dimensions match if frame.shape[:2] != (height, width): frame = cv2.resize(frame, (width, height)) video_writer.write(frame) frames_written += 1 if i % 10 == 0: # Update progress every 10 frames progress = 85 + (i / len(frame_files)) * 3 update_status(progress, f"Writing frame {i+1}/{len(frame_files)} with {codec}") video_writer.release() # Check if video file was created and has reasonable size if os.path.exists(test_output_path) and os.path.getsize(test_output_path) > 1024: output_video_path = test_output_path video_created = True update_status(88, f"Video created successfully with {codec} ({frames_written} frames)") break else: update_status(86, f"Video file not created or too small with {codec}") except Exception as codec_error: update_status(87, f"Error with {codec}: {str(codec_error)}") continue if not video_created: raise Exception("Failed to create video with any codec") except Exception as video_error: raise Exception(f"Video creation failed: {str(video_error)}") update_status(90, "Cleaning up temporary files...") # Clean up frame files for frame_file in frame_files: try: os.remove(frame_file) except: pass try: os.rmdir(frames_folder) except: pass update_status(100, "Progressive 3D animation complete!") def show_success(dt): popup.dismiss() self.show_success_popup( "Progressive 3D Animation Complete!", f"Your progressive 3D animation has been saved to:\n{output_video_path}\n\nThe animation shows {len(positions)} GPS points building the route progressively from start to finish.", output_video_path ) Clock.schedule_once(show_success, 1) else: raise Exception("No frames were generated") except Exception as e: error_message = str(e) print(f"DEBUG: Progressive animation error: {error_message}") import traceback traceback.print_exc() def show_error(dt): popup.dismiss() self.show_error_popup("Progressive Animation Error", error_message) Clock.schedule_once(show_error, 0) # Schedule the animation generation Clock.schedule_once(lambda dt: run_progressive_animation(), 0.5) def open_pauses_popup(self): """Navigate to the pause edit screen""" pause_edit_screen = self.manager.get_screen("pause_edit") pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter) self.manager.current = "pause_edit" def show_success_popup(self, title, message, file_path): """Show success popup with option to open file location""" layout = BoxLayout(orientation='vertical', spacing=10, padding=15) # Success message success_label = Label( text=message, text_size=(None, None), halign="center", valign="middle" ) layout.add_widget(success_label) # Buttons btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50) open_folder_btn = Button( text="Open Folder", background_color=(0.2, 0.6, 0.9, 1) ) ok_btn = Button( text="OK", background_color=(0.3, 0.7, 0.3, 1) ) btn_layout.add_widget(open_folder_btn) btn_layout.add_widget(ok_btn) layout.add_widget(btn_layout) popup = Popup( title=title, content=layout, size_hint=(0.9, 0.6), auto_dismiss=False ) def open_folder(instance): folder_path = os.path.dirname(file_path) os.system(f'xdg-open "{folder_path}"') # Linux popup.dismiss() def close_popup(instance): popup.dismiss() open_folder_btn.bind(on_press=open_folder) ok_btn.bind(on_press=close_popup) popup.open() def show_error_popup(self, title, message): """Show error popup""" layout = BoxLayout(orientation='vertical', spacing=10, padding=15) error_label = Label( text=f"Error: {message}", text_size=(None, None), halign="center", valign="middle" ) layout.add_widget(error_label) ok_btn = Button( text="OK", background_color=(0.8, 0.3, 0.3, 1), size_hint_y=None, height=50 ) layout.add_widget(ok_btn) popup = Popup( title=title, content=layout, size_hint=(0.8, 0.4), auto_dismiss=False ) ok_btn.bind(on_press=lambda x: popup.dismiss()) popup.open()