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.video_3d_generator import generate_3d_video_animation from py_scripts.advanced_3d_generator import Advanced3DGenerator from py_scripts.blender_animator import BlenderGPSAnimator 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 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 generate_3d_video(self): """Show video generation mode selection popup""" self.show_video_generation_options() def generate_3d_video_test_mode(self): """Generate a 3D video animation in 720p test mode for faster processing""" # Show processing popup with test mode indication layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Preparing 720p test video generation...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Generating 3D Video Animation (720p Test Mode)", content=layout, size_hint=(0.9, None), size=(0, 200), auto_dismiss=False ) popup.open() # Schedule the 3D video generation in test mode Clock.schedule_once( lambda dt: generate_3d_video_animation( self.project_name, RESOURCES_FOLDER, label, progress, popup, Clock, test_mode=True # Enable test mode ), 0.5 ) def generate_3d_video_production_mode(self): """Generate a 3D video animation in 2K production mode for high quality""" # Show processing popup with production mode indication layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Preparing 2K production video generation...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Generating 3D Video Animation (2K Production Mode)", content=layout, size_hint=(0.9, None), size=(0, 200), auto_dismiss=False ) popup.open() # Schedule the 3D video generation in production mode Clock.schedule_once( lambda dt: generate_3d_video_animation( self.project_name, RESOURCES_FOLDER, label, progress, popup, Clock, test_mode=False # Disable test mode for production ), 0.5 ) def show_video_generation_options(self): """Show popup with video generation mode options including new advanced animations""" from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.label import Label layout = BoxLayout(orientation='vertical', spacing=12, padding=15) # Title title_label = Label( text="Choose Animation Style & Quality", font_size=20, size_hint_y=None, height=40, color=(1, 1, 1, 1) ) layout.add_widget(title_label) # Classic 3D Mode classic_layout = BoxLayout(orientation='vertical', spacing=5) classic_title = Label( text="🏃‍♂️ Classic 3D (Original Pipeline)", font_size=16, size_hint_y=None, height=30, color=(0.2, 0.8, 0.2, 1) ) classic_desc = Label( text="• Traditional OpenCV/PIL approach\n• Fast generation\n• Good for simple tracks\n• Test (720p) or Production (2K)", font_size=11, size_hint_y=None, height=70, color=(0.9, 0.9, 0.9, 1), halign="left", valign="middle" ) classic_desc.text_size = (None, None) classic_layout.add_widget(classic_title) classic_layout.add_widget(classic_desc) layout.add_widget(classic_layout) # Classic buttons classic_btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=45) classic_test_btn = Button( text="Classic 720p", background_color=(0.2, 0.8, 0.2, 1), font_size=12 ) classic_prod_btn = Button( text="Classic 2K", background_color=(0.3, 0.6, 0.3, 1), font_size=12 ) classic_btn_layout.add_widget(classic_test_btn) classic_btn_layout.add_widget(classic_prod_btn) layout.add_widget(classic_btn_layout) # Advanced Pydeck/Plotly Mode advanced_layout = BoxLayout(orientation='vertical', spacing=5) advanced_title = Label( text="🚀 Advanced 3D (Pydeck + Plotly)", font_size=16, size_hint_y=None, height=30, color=(0.2, 0.6, 0.9, 1) ) advanced_desc = Label( text="• Professional geospatial visualization\n• Interactive 3D terrain\n• Advanced camera movements\n• High-quality animations", font_size=11, size_hint_y=None, height=70, color=(0.9, 0.9, 0.9, 1), halign="left", valign="middle" ) advanced_desc.text_size = (None, None) advanced_layout.add_widget(advanced_title) advanced_layout.add_widget(advanced_desc) layout.add_widget(advanced_layout) # Advanced button advanced_btn = Button( text="Generate Advanced 3D Animation", background_color=(0.2, 0.6, 0.9, 1), size_hint_y=None, height=45, font_size=13 ) layout.add_widget(advanced_btn) # Blender Cinema Mode blender_layout = BoxLayout(orientation='vertical', spacing=5) blender_title = Label( text="� Cinema Quality (Blender)", font_size=16, size_hint_y=None, height=30, color=(0.9, 0.6, 0.2, 1) ) blender_desc = Label( text="• Professional 3D rendering\n• Photorealistic visuals\n• Cinema-grade quality\n• Longer processing time", font_size=11, size_hint_y=None, height=70, color=(0.9, 0.9, 0.9, 1), halign="left", valign="middle" ) blender_desc.text_size = (None, None) blender_layout.add_widget(blender_title) blender_layout.add_widget(blender_desc) layout.add_widget(blender_layout) # Blender button blender_btn = Button( text="Generate Blender Cinema Animation", background_color=(0.9, 0.6, 0.2, 1), size_hint_y=None, height=45, font_size=13 ) layout.add_widget(blender_btn) # Cancel button cancel_btn = Button( text="Cancel", background_color=(0.5, 0.5, 0.5, 1), size_hint_y=None, height=40, font_size=12 ) layout.add_widget(cancel_btn) popup = Popup( title="Select Animation Style", content=layout, size_hint=(0.95, 0.9), auto_dismiss=False ) def start_classic_test(instance): popup.dismiss() self.generate_3d_video_test_mode() def start_classic_production(instance): popup.dismiss() self.generate_3d_video_production_mode() def start_advanced_3d(instance): popup.dismiss() self.generate_advanced_3d_animation() def start_blender_animation(instance): popup.dismiss() self.generate_blender_animation() 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) blender_btn.bind(on_press=start_blender_animation) cancel_btn.bind(on_press=lambda x: popup.dismiss()) popup.open() def generate_advanced_3d_animation(self): """Generate advanced 3D animation using Pydeck and Plotly""" # Show processing popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Initializing advanced 3D animation...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Generating Advanced 3D Animation", content=layout, size_hint=(0.9, None), size=(0, 200), auto_dismiss=False ) popup.open() def run_advanced_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 = Advanced3DGenerator(project_folder) generator.check_dependencies() update_status(20, "Processing GPS coordinates...") df = generator.load_gps_data(positions_path) update_status(40, "Creating 3D visualization frames...") output_video_path = os.path.join(project_folder, f"{self.project_name}_advanced_3d_{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.4), message) # Map 0-100% to 40-80% update_status(80, "Rendering video...") success = generator.generate_3d_animation( positions_path, output_video_path, style='advanced', progress_callback=generator_progress ) if success: update_status(100, "Advanced 3D animation complete!") output_path = output_video_path else: raise Exception("Failed to generate video") def show_success(dt): popup.dismiss() self.show_success_popup( "Advanced 3D Animation Complete!", f"Your high-quality 3D animation 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("Advanced Animation Error", error_message) Clock.schedule_once(show_error, 0) # Schedule the animation generation Clock.schedule_once(lambda dt: run_advanced_animation(), 0.5) def generate_blender_animation(self): """Generate cinema-quality animation using Blender""" # Show processing popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Initializing Blender rendering pipeline...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Generating Blender 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 update_status(10, "Loading GPS data into Blender...") # Check dependencies first 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 render Blender animation") def show_success(dt): popup.dismiss() self.show_success_popup( "Blender Cinema Animation Complete!", f"Your cinema-quality animation has been rendered 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("Blender 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 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) success_label = Label( text=message, text_size=(400, None), halign="center", valign="middle" ) layout.add_widget(success_label) button_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50) if file_path: open_btn = Button(text="Open Folder", background_color=(0.2, 0.8, 0.2, 1)) open_btn.bind(on_press=lambda x: self.open_file_location(file_path)) button_layout.add_widget(open_btn) ok_btn = Button(text="OK", background_color=(0.2, 0.6, 0.9, 1)) button_layout.add_widget(ok_btn) layout.add_widget(button_layout) popup = Popup( title=title, content=layout, size_hint=(0.8, None), size=(0, 250), auto_dismiss=False ) ok_btn.bind(on_press=lambda x: popup.dismiss()) popup.open() def show_error_popup(self, title, message): """Show error popup""" layout = BoxLayout(orientation='vertical', spacing=10, padding=10) error_label = Label( text=f"Error: {message}", text_size=(400, None), halign="center", valign="middle", color=(1, 0.3, 0.3, 1) ) layout.add_widget(error_label) ok_btn = Button(text="OK", background_color=(0.8, 0.2, 0.2, 1), size_hint_y=None, height=50) layout.add_widget(ok_btn) popup = Popup( title=title, content=layout, size_hint=(0.8, None), size=(0, 200), auto_dismiss=False ) ok_btn.bind(on_press=lambda x: popup.dismiss()) popup.open() def open_file_location(self, file_path): """Open file location in system file manager""" import subprocess import platform folder_path = os.path.dirname(file_path) try: if platform.system() == "Linux": subprocess.run(["xdg-open", folder_path]) elif platform.system() == "Darwin": # macOS subprocess.run(["open", folder_path]) elif platform.system() == "Windows": subprocess.run(["explorer", folder_path]) except Exception as e: print(f"Could not open folder: {e}")