Files
traccar_animation/screens/create_animation_screen.py
2025-07-10 15:26:18 +03:00

942 lines
44 KiB
Python

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()