942 lines
44 KiB
Python
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()
|