Compare commits

..

7 Commits

Author SHA1 Message Date
7df9de12ce Repository cleanup: Remove large files, cache files, sensitive data, and junk files
- Updated .gitignore with comprehensive patterns for Python, media files, and project data
- Removed all __pycache__ directories and .pyc files from tracking
- Removed large media files (PNG frames, MP4 videos) from git history
- Removed sensitive credential files (credentials.enc, key.key, server_settings.enc)
- Removed test files and temporary data from tracking
- Removed junk_files directory from tracking
- Repository size optimization and security improvement
2025-07-15 15:20:39 +03:00
9f8c1c27dc saved 2025-07-15 14:55:51 +03:00
1d0dc05a7b uploaded 2025-07-10 15:26:18 +03:00
911143dfc5 updated strategi 2025-07-10 13:50:44 +03:00
29fd68f732 updated to generate trip 2025-07-10 13:46:05 +03:00
4fa7ed2a48 updated v 2025-07-09 16:47:17 +03:00
35d3bb8442 updated versions 2025-07-09 16:39:51 +03:00
33 changed files with 3490 additions and 139139 deletions

39
.gitignore vendored
View File

@@ -1,2 +1,39 @@
# Ignore the virtual environment folder
track/
track/
# Ignore Python cache files
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# Ignore project data and generated files
resources/projects/
resources/trip_archive/
resources/credentials.enc
resources/key.key
resources/server_settings.enc
# Ignore generated videos and frames
*.mp4
*.avi
*.mov
*.webm
cinema_frames/
progressive_frames/
# Ignore test files and temporary files
test_*.py
*.tmp
*.log
# Ignore IDE files
.vscode/
.idea/
*.swp
*.swo
# Ignore OS files
.DS_Store
Thumbs.db

View File

@@ -1,45 +0,0 @@
# Project Cleanup Summary
## What Was Cleaned Up
### Moved to `junk_files/`
- Documentation files (*.md) that were cluttering the root directory
- `3D_VIDEO_DOCUMENTATION.md`
- `PAUSE_EDIT_IMPROVEMENTS.md`
- `PROJECT_MODERNIZATION_SUMMARY.md`
- `TEST_MODE_DOCUMENTATION.md`
### Removed
- All `__pycache__` directories and compiled Python bytecode files
- Duplicate and test files that were no longer needed
### Fixed
- Fixed typo in requirements.txt (`reqirements.txt` was corrected to `requirements.txt`)
- Ensured proper import structure (app uses `py_scripts.video_3d_generator` correctly)
## Current Clean Structure
```
traccar_animation/
├── .git/ # Git repository files
├── .gitignore # Git ignore rules
├── config.py # Application configuration
├── main.py # Main application entry point
├── traccar.kv # Kivy UI layout file
├── requirements.txt # Python dependencies (fixed)
├── py_scripts/ # Python modules
│ ├── __init__.py
│ ├── utils.py
│ ├── video_3d_generator.py
│ └── webview.py
├── screens/ # Kivy screen modules
├── resources/ # Application resources
├── track/ # Virtual environment
└── junk_files/ # Non-essential files moved here
```
## Verification
- ✅ Utils module imports correctly
- ✅ Video 3D generator module imports correctly
- ✅ No duplicate files remain
- ✅ All dependencies properly listed in requirements.txt
- ✅ Clean project structure maintained

Binary file not shown.

View File

@@ -1,45 +0,0 @@
# Project Cleanup Summary
## What Was Cleaned Up
### Moved to `junk_files/`
- Documentation files (*.md) that were cluttering the root directory
- `3D_VIDEO_DOCUMENTATION.md`
- `PAUSE_EDIT_IMPROVEMENTS.md`
- `PROJECT_MODERNIZATION_SUMMARY.md`
- `TEST_MODE_DOCUMENTATION.md`
### Removed
- All `__pycache__` directories and compiled Python bytecode files
- Duplicate and test files that were no longer needed
### Fixed
- Fixed typo in requirements.txt (`reqirements.txt` was corrected to `requirements.txt`)
- Ensured proper import structure (app uses `py_scripts.video_3d_generator` correctly)
## Current Clean Structure
```
traccar_animation/
├── .git/ # Git repository files
├── .gitignore # Git ignore rules
├── config.py # Application configuration
├── main.py # Main application entry point
├── traccar.kv # Kivy UI layout file
├── requirements.txt # Python dependencies (fixed)
├── py_scripts/ # Python modules
│ ├── __init__.py
│ ├── utils.py
│ ├── video_3d_generator.py
│ └── webview.py
├── screens/ # Kivy screen modules
├── resources/ # Application resources
├── track/ # Virtual environment
└── junk_files/ # Non-essential files moved here
```
## Verification
- ✅ Utils module imports correctly
- ✅ Video 3D generator module imports correctly
- ✅ No duplicate files remain
- ✅ All dependencies properly listed in requirements.txt
- ✅ Clean project structure maintained

View File

@@ -1,105 +0,0 @@
# Traccar Animation App - Modernization Complete
## Project Overview
The Traccar Animation App has been successfully modernized with enhanced 3D video animation capabilities, improved code structure, and streamlined codebase.
## Completed Modernization Tasks
### 1. Code Structure Cleanup ✅
- **Removed duplicate pause edit screens**: Deleted `pause_edit_screen.py` and `pause_edit_screen_legacy.py`
- **Single source of truth**: Only `pause_edit_screen_improved.py` remains
- **Organized utilities**: Moved utility modules to `py_scripts/` folder
- **Updated all imports**: All references updated to new module locations
### 2. Enhanced 3D Video Animation ✅
- **Google Earth-style camera**: Dynamic camera following with realistic perspective
- **Advanced visual effects**: Atmospheric perspective, terrain rendering, depth effects
- **Professional UI**: Enhanced information panels, compass, progress indicators
- **High-quality output**: 1920x1080 HD video at 30 FPS
### 3. Project Structure Improvements ✅
```
traccar_animation/
├── main.py # Main application entry
├── config.py # Configuration management
├── traccar.kv # UI layout definitions
├── reqirements.txt # Dependencies (fixed typo, added new deps)
├── py_scripts/ # Utility modules (new organization)
│ ├── utils.py # Core utilities
│ ├── video_3d_generator.py # Enhanced 3D video engine
│ ├── webview.py # Web integration
│ └── 3D_VIDEO_DOCUMENTATION.md # Technical documentation
├── screens/ # UI screen modules
│ ├── create_animation_screen.py
│ ├── get_trip_from_server.py
│ ├── home_screen.py
│ ├── login_screen.py
│ ├── pause_edit_screen_improved.py # Single pause edit implementation
│ └── settings_screen.py
└── resources/ # Static resources and data
├── images/
├── projects/
└── trip_archive/
```
### 4. Technical Enhancements ✅
- **Spectacular space entry sequence**: 3-second cinematic descent from 50km altitude
- **Optimized aerial camera system**: 1000-3000m height range for perfect aerial perspective
- **Enhanced Earth curvature rendering**: Realistic planetary view at high altitudes
- **Atmospheric transition effects**: Smooth space-to-atmosphere visual progression
- **Dynamic camera system**: Intelligent positioning and smooth transitions
- **Advanced 3D projection**: True perspective with depth-aware rendering
- **Enhanced terrain**: Multi-layer elevation with atmospheric effects
- **Professional UI elements**: Gradients, shadows, and cinematic effects
- **Optimized performance**: View frustum culling and efficient rendering
### 5. Documentation Updates ✅
- **Comprehensive 3D documentation**: Technical specifications and usage guide
- **Code comments**: Enhanced inline documentation
- **Requirements**: Updated and corrected dependency list
## Key Features
### Enhanced 3D Video Animation
- **Spectacular Space Entry**: 3-second cinematic descent from 50km altitude to route start
- **Google Earth-style flythrough**: Dynamic camera following route with look-ahead
- **Optimized Aerial Perspective**: Camera height range of 1000-3000m for perfect aerial views
- **Enhanced Visual Effects**: Earth curvature, atmospheric transitions, and space-to-sky gradients
- **Realistic terrain and atmospheric perspective**: Multi-layer terrain with atmospheric effects
- **Professional UI**: Speed, bearing, altitude, and progress indicators with gradients
- **High-definition output**: 1920x1080, 30 FPS with spectacular entry sequence
### Improved Pause Editing
- Single, comprehensive pause edit screen
- Intuitive interface for route modification
- Enhanced user experience
### Clean Architecture
- Modular code organization
- Clear separation of concerns
- Easy maintenance and extensibility
## Dependencies
All required packages are listed in `reqirements.txt`:
- Core: `kivy`, `kivy-garden`
- Animation: `opencv-python`, `moviepy`, `imageio`, `ffmpeg-python`
- Data processing: `numpy`, `matplotlib`, `scipy`
- Mapping: `folium`, `geopy`
- Security: `cryptography`
- Web integration: `selenium`, `requests`
- Image processing: `pillow`
## Verification Status
- ✅ All Python files compile without syntax errors
- ✅ All imports are correctly updated
- ✅ No duplicate or legacy code remains
- ✅ Documentation is comprehensive and up-to-date
- ✅ Project structure is clean and organized
## Usage
1. Install dependencies: `pip install -r reqirements.txt`
2. Run the application: `python main.py`
3. Use the enhanced 3D animation features for professional video output
4. Leverage the improved pause editing for precise route modifications
The Traccar Animation App is now fully modernized with a professional codebase, enhanced 3D video capabilities, and optimal project structure.

View File

@@ -1,108 +0,0 @@
# 3D Video Generation Test Mode
## Overview
The 3D video generation now supports two modes to balance quality and generation speed:
### 🏃‍♂️ 720p Test Mode (Fast)
- **Resolution**: 1280x720 pixels
- **Frame Rate**: 30 FPS
- **Entry Sequence**: 60 frames (2 seconds)
- **Route Frames**: 2x per GPS point
- **Generation Speed**: ~3x faster than production mode
- **File Size**: ~1/4 of production mode
- **Best For**: Quick previews, debugging routes, testing changes
### 🎯 2K Production Mode (High Quality)
- **Resolution**: 2560x1440 pixels (2K)
- **Frame Rate**: 60 FPS
- **Entry Sequence**: 120 frames (4 seconds)
- **Route Frames**: 3x per GPS point
- **Generation Speed**: Full quality processing
- **File Size**: Full size for maximum quality
- **Best For**: Final videos, presentations, high-quality output
## How to Use
### In the App UI
1. Click "Generate 3D Video" button
2. Choose from the popup:
- **"Generate 720p Test Video"** for fast testing
- **"Generate 2K Production Video"** for final quality
### In Code
```python
# Test mode (720p, faster)
generate_3d_video_animation(project_name, resources_folder, label, progress, popup, clock, test_mode=True)
# Production mode (2K, high quality)
generate_3d_video_animation(project_name, resources_folder, label, progress, popup, clock, test_mode=False)
# Or use convenience functions
generate_3d_video_animation_test_mode(...)
generate_3d_video_animation_production_mode(...)
```
## Performance Comparison
| Aspect | 720p Test Mode | 2K Production Mode |
|--------|----------------|-------------------|
| Resolution | 1280x720 | 2560x1440 |
| Total Pixels | ~0.9 megapixels | ~3.7 megapixels |
| Frame Rate | 30 FPS | 60 FPS |
| Space Entry | 2 seconds | 4 seconds |
| Processing Time | ~3x faster | Full quality |
| File Size | ~1/4 size | Full size |
| Quality | Good for preview | Cinema quality |
## When to Use Each Mode
### Use 720p Test Mode When:
- ✅ Testing route visualization
- ✅ Debugging GPS data issues
- ✅ Iterating on video parameters
- ✅ Quick previews for clients
- ✅ Development and testing
- ✅ Limited storage space
- ✅ Faster upload/sharing needed
### Use 2K Production Mode When:
- ✅ Creating final deliverable videos
- ✅ Professional presentations
- ✅ High-quality demos
- ✅ Maximum visual impact needed
- ✅ Detailed route analysis required
- ✅ Large screen display planned
## File Naming Convention
Generated videos will include the mode in the filename:
- Test mode: `project_720p_test_20250708_142815.mp4`
- Production mode: `project_2K_production_20250708_142815.mp4`
## Technical Details
### Test Mode Optimizations:
- Reduced frame generation (60 vs 120 for entry)
- Lower resolution reduces processing per frame
- Fewer intermediate frames per GPS point
- 30 FPS reduces total frame count
- Optimized rendering pipeline
### Production Mode Features:
- Ultra-high resolution Earth rendering
- Extended space entry sequence
- Maximum detail in atmospheric effects
- Professional-grade visual effects
- Cinema-quality color grading
- Smooth 60 FPS motion
## Tips for Best Results
1. **Start with Test Mode**: Always preview your route in 720p test mode first
2. **Iterate Quickly**: Use test mode to adjust route parameters
3. **Final Production**: Once satisfied, generate the 2K production version
4. **Storage Planning**: Test mode files are ~25% the size of production files
5. **Time Management**: Test mode generates ~3x faster than production mode
This dual-mode approach allows for rapid iteration during development while maintaining the ability to produce ultra-high-quality final videos.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,333 @@
import json
import numpy as np
import os
from datetime import datetime
import math
# Blender dependencies with fallback handling
try:
import bpy
import bmesh
from mathutils import Vector, Euler
BLENDER_AVAILABLE = True
except ImportError:
BLENDER_AVAILABLE = False
print("Warning: Blender (bpy) not available. This module requires Blender to be installed with Python API access.")
class BlenderGPSAnimator:
"""
Advanced GPS track animation using Blender for high-quality 3D rendering
"""
def __init__(self, output_folder):
self.output_folder = output_folder
if BLENDER_AVAILABLE:
self.setup_blender_scene()
else:
# Don't raise error here, let the caller handle the check
pass
def check_dependencies(self):
"""Check if Blender dependencies are available"""
if not BLENDER_AVAILABLE:
raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.")
return True
def setup_blender_scene(self):
"""Setup Blender scene for GPS animation"""
# Clear existing mesh objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
# Add camera
bpy.ops.object.camera_add(location=(0, 0, 10))
self.camera = bpy.context.object
# Add sun light
bpy.ops.object.light_add(type='SUN', location=(0, 0, 20))
light = bpy.context.object
light.data.energy = 5
# Setup world environment
world = bpy.context.scene.world
world.use_nodes = True
env_texture = world.node_tree.nodes.new('ShaderNodeTexEnvironment')
world.node_tree.links.new(env_texture.outputs[0], world.node_tree.nodes['Background'].inputs[0])
# Set render settings
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.render.fps = 30
scene.cycles.samples = 64
def load_gps_data(self, positions_file):
"""Load GPS data from JSON file"""
with open(positions_file, 'r') as f:
positions = json.load(f)
# Convert to numpy array for easier processing
coords = []
times = []
speeds = []
for pos in positions:
coords.append([pos['longitude'], pos['latitude'], pos.get('altitude', 0)])
times.append(pos['fixTime'])
speeds.append(pos.get('speed', 0) * 1.852) # Convert to km/h
return np.array(coords), times, speeds
def create_terrain_mesh(self, coords):
"""Create a simple terrain mesh based on GPS bounds"""
# Calculate bounds
min_lon, min_lat = coords[:, :2].min(axis=0)
max_lon, max_lat = coords[:, :2].max(axis=0)
# Expand bounds slightly
padding = 0.001
min_lon -= padding
min_lat -= padding
max_lon += padding
max_lat += padding
# Create terrain mesh
bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 0, 0))
terrain = bpy.context.object
terrain.name = "Terrain"
# Scale terrain to match GPS bounds
lon_range = max_lon - min_lon
lat_range = max_lat - min_lat
scale_factor = max(lon_range, lat_range) * 100000 # Convert to reasonable scale
terrain.scale = (scale_factor, scale_factor, 1)
# Apply material
mat = bpy.data.materials.new(name="TerrainMaterial")
mat.use_nodes = True
mat.node_tree.nodes.clear()
# Add principled BSDF
bsdf = mat.node_tree.nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf.inputs['Base Color'].default_value = (0.2, 0.5, 0.2, 1.0) # Green
bsdf.inputs['Roughness'].default_value = 0.8
material_output = mat.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
mat.node_tree.links.new(bsdf.outputs['BSDF'], material_output.inputs['Surface'])
terrain.data.materials.append(mat)
return terrain
def create_gps_track_mesh(self, coords):
"""Create a 3D mesh for the GPS track"""
# Normalize coordinates to Blender scale
coords_normalized = self.normalize_coordinates(coords)
# Create curve from GPS points
curve_data = bpy.data.curves.new('GPSTrack', type='CURVE')
curve_data.dimensions = '3D'
curve_data.bevel_depth = 0.02
curve_data.bevel_resolution = 4
# Create spline
spline = curve_data.splines.new('BEZIER')
spline.bezier_points.add(len(coords_normalized) - 1)
for i, coord in enumerate(coords_normalized):
point = spline.bezier_points[i]
point.co = coord
point.handle_left_type = 'AUTO'
point.handle_right_type = 'AUTO'
# Create object from curve
track_obj = bpy.data.objects.new('GPSTrack', curve_data)
bpy.context.collection.objects.link(track_obj)
# Apply material
mat = bpy.data.materials.new(name="TrackMaterial")
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
bsdf.inputs['Base Color'].default_value = (1.0, 0.0, 0.0, 1.0) # Red
bsdf.inputs['Emission'].default_value = (1.0, 0.2, 0.2, 1.0)
bsdf.inputs['Emission Strength'].default_value = 2.0
track_obj.data.materials.append(mat)
return track_obj
def create_vehicle_model(self):
"""Create a simple vehicle model"""
# Create a simple car shape using cubes
bpy.ops.mesh.primitive_cube_add(size=0.1, location=(0, 0, 0.05))
vehicle = bpy.context.object
vehicle.name = "Vehicle"
vehicle.scale = (2, 1, 0.5)
# Apply material
mat = bpy.data.materials.new(name="VehicleMaterial")
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
bsdf.inputs['Base Color'].default_value = (0.0, 0.0, 1.0, 1.0) # Blue
bsdf.inputs['Metallic'].default_value = 0.5
bsdf.inputs['Roughness'].default_value = 0.2
vehicle.data.materials.append(mat)
return vehicle
def normalize_coordinates(self, coords):
"""Normalize GPS coordinates to Blender scale"""
# Center coordinates
center = coords.mean(axis=0)
coords_centered = coords - center
# Scale to reasonable size for Blender
scale_factor = 100
coords_scaled = coords_centered * scale_factor
# Convert to Vector objects
return [Vector((x, y, z)) for x, y, z in coords_scaled]
def animate_vehicle(self, vehicle, coords, times, speeds):
"""Create animation keyframes for vehicle movement"""
coords_normalized = self.normalize_coordinates(coords)
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = len(coords_normalized) * 2 # 2 frames per GPS point
for i, (coord, speed) in enumerate(zip(coords_normalized, speeds)):
frame = i * 2 + 1
# Set location
vehicle.location = coord
vehicle.keyframe_insert(data_path="location", frame=frame)
# Calculate rotation based on direction
if i < len(coords_normalized) - 1:
next_coord = coords_normalized[i + 1]
direction = next_coord - coord
if direction.length > 0:
direction.normalize()
# Calculate rotation angle
angle = math.atan2(direction.y, direction.x)
vehicle.rotation_euler = Euler((0, 0, angle), 'XYZ')
vehicle.keyframe_insert(data_path="rotation_euler", frame=frame)
# Set interpolation mode
if vehicle.animation_data:
for fcurve in vehicle.animation_data.action.fcurves:
for keyframe in fcurve.keyframe_points:
keyframe.interpolation = 'BEZIER'
def animate_camera(self, coords):
"""Create smooth camera animation following the vehicle"""
coords_normalized = self.normalize_coordinates(coords)
# Create camera path
for i, coord in enumerate(coords_normalized):
frame = i * 2 + 1
# Position camera above and behind the vehicle
offset = Vector((0, -2, 3))
cam_location = coord + offset
self.camera.location = cam_location
self.camera.keyframe_insert(data_path="location", frame=frame)
# Look at the vehicle
direction = coord - cam_location
if direction.length > 0:
rot_quat = direction.to_track_quat('-Z', 'Y')
self.camera.rotation_euler = rot_quat.to_euler()
self.camera.keyframe_insert(data_path="rotation_euler", frame=frame)
def add_particles_effects(self, vehicle):
"""Add particle effects for enhanced visuals"""
# Add dust particles
bpy.context.view_layer.objects.active = vehicle
bpy.ops.object.modifier_add(type='PARTICLE_SYSTEM')
particles = vehicle.modifiers["ParticleSystem"].particle_system
particles.settings.count = 100
particles.settings.lifetime = 30
particles.settings.emit_from = 'FACE'
particles.settings.physics_type = 'NEWTON'
particles.settings.effector_weights.gravity = 0.1
# Set material for particles
particles.settings.material = 1
def render_animation(self, output_path, progress_callback=None):
"""Render the animation to video"""
scene = bpy.context.scene
# Set output settings
scene.render.filepath = output_path
scene.render.image_settings.file_format = 'FFMPEG'
scene.render.ffmpeg.format = 'MPEG4'
scene.render.ffmpeg.codec = 'H264'
# Render animation
total_frames = scene.frame_end - scene.frame_start + 1
for frame in range(scene.frame_start, scene.frame_end + 1):
scene.frame_set(frame)
# Render frame
frame_path = f"{output_path}_{frame:04d}.png"
scene.render.filepath = frame_path
bpy.ops.render.render(write_still=True)
# Update progress
if progress_callback:
progress = ((frame - scene.frame_start) / total_frames) * 100
progress_callback(progress, f"Rendering frame {frame}/{scene.frame_end}")
def create_gps_animation(self, positions_file, output_path, progress_callback=None):
"""Main method to create GPS animation in Blender"""
try:
# Load GPS data
coords, times, speeds = self.load_gps_data(positions_file)
# Create scene elements
terrain = self.create_terrain_mesh(coords)
track = self.create_gps_track_mesh(coords)
vehicle = self.create_vehicle_model()
# Create animations
self.animate_vehicle(vehicle, coords, times, speeds)
self.animate_camera(coords)
# Add effects
self.add_particles_effects(vehicle)
# Render animation
self.render_animation(output_path, progress_callback)
return True
except Exception as e:
print(f"Error creating Blender animation: {e}")
if progress_callback:
progress_callback(-1, f"Error: {e}")
return False
def generate_blender_animation(positions_file, output_folder, progress_callback=None):
"""
Convenience function to generate Blender animation
"""
animator = BlenderGPSAnimator(output_folder)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = os.path.join(output_folder, f"blender_animation_{timestamp}")
success = animator.create_gps_animation(positions_file, output_path, progress_callback)
return f"{output_path}.mp4" if success else None
# Note: This script should be run from within Blender's Python environment
# or with Blender as a Python module (bpy)

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,15 @@ selenium
pillow
geopy
opencv-python
moviepy
requests
numpy
matplotlib
scipy
imageio
ffmpeg-python
ffmpeg-python
pydeck
plotly
dash
pandas
geopandas
bpy

View File

@@ -1 +0,0 @@
gAAAAABobfofcr10BIPwspryfc740kIyIDl3sH0B0Jb598Zc9boEPMP01OyKqPXI1Dcfrqu6KGUI0useWSTQanKWBjCLNY-jQZmGKvbRRWL03bVhFl0i_5qUwgmLNHMSSXZi5U9oXFo7

View File

@@ -1 +0,0 @@
wetp_PNG9CC5432-W9H3rUbaqIurwldZxHlOgori5kY=

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1 +0,0 @@
gAAAAABobK2fcNGeWyfPJzYnOl_HWl8TdQfRDb5teUXH9Kpjmme0TUVA3Dy7wm2MuMEGsPBTWBm8XfaX8daIwu6iDV6o8G07XZ_A0RoMqx3xWiYUbX63ovYy8qITIpMqbt0dayYigDSPmdr_8pcqko6ik-ctfdg4SkGH1gRXb5yuacnzezLr3KcHMh833PkbTO6WiUYPCwaivEMTVHUxL5YORiLRGu4E3lS_WDPo7kv53khtUI9b7vWJOOUFXcelM2vF3iHI3EkXCWrO2Qpm22nC44b-yCnZvYzx7g-WHZDNfG6CA1KXbcyhouxR4b7502iofpEAN5sizLFuyOWIOBdVphblIkRd1qdq6fVmt0IMeoaMpNPNuDKJqMDLuAU05wXDWbGXei6YU6rs6YJgpGOfNdv8A_sKKJBrh5QVE2kZ2GE0Ysqpnw2Yfj_jsMBpdh-bBs6UDwcI

View File

@@ -3,12 +3,14 @@ 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 NavigationAnimationGenerator
# BlenderGPSAnimator imported conditionally when needed
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.label import Label
@@ -48,8 +50,6 @@ class CreateAnimationScreen(Screen):
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
@@ -128,7 +128,6 @@ class CreateAnimationScreen(Screen):
on_save=lambda: self.on_pre_enter()
)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
@@ -165,191 +164,778 @@ class CreateAnimationScreen(Screen):
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 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()
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)
# 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"""
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=15, padding=15)
# Title
title_label = Label(
text="Choose Video Generation Mode",
font_size=18,
size_hint_y=None,
height=40,
color=(1, 1, 1, 1)
)
layout.add_widget(title_label)
# Test mode description
test_layout = BoxLayout(orientation='vertical', spacing=5)
test_title = Label(
text="🏃‍♂️ 720p Test Mode (Fast)",
font_size=16,
size_hint_y=None,
height=30,
color=(0.2, 0.8, 0.2, 1)
)
test_desc = Label(
text="• Resolution: 1280x720\n• Frame rate: 30 FPS\n• ~3x faster generation\n• Perfect for quick previews",
font_size=12,
size_hint_y=None,
height=80,
color=(0.9, 0.9, 0.9, 1),
halign="left",
# Success message
success_label = Label(
text=message,
text_size=(None, None),
halign="center",
valign="middle"
)
test_desc.text_size = (None, None)
test_layout.add_widget(test_title)
test_layout.add_widget(test_desc)
layout.add_widget(test_layout)
layout.add_widget(success_label)
# Test mode button
test_btn = Button(
text="Generate 720p Test Video",
background_color=(0.2, 0.8, 0.2, 1),
size_hint_y=None,
height=50,
font_size=14
)
layout.add_widget(test_btn)
# Buttons
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
# Production mode description
prod_layout = BoxLayout(orientation='vertical', spacing=5)
prod_title = Label(
text="🎯 2K Production Mode (High Quality)",
font_size=16,
size_hint_y=None,
height=30,
color=(0.8, 0.2, 0.2, 1)
open_folder_btn = Button(
text="Open Folder",
background_color=(0.2, 0.6, 0.9, 1)
)
prod_desc = Label(
text="• Resolution: 2560x1440\n• Frame rate: 60 FPS\n• Cinema-quality results\n• Ultra-detailed visuals",
font_size=12,
size_hint_y=None,
height=80,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
prod_desc.text_size = (None, None)
prod_layout.add_widget(prod_title)
prod_layout.add_widget(prod_desc)
layout.add_widget(prod_layout)
# Production mode button
prod_btn = Button(
text="Generate 2K Production Video",
background_color=(0.8, 0.2, 0.2, 1),
size_hint_y=None,
height=50,
font_size=14
ok_btn = Button(
text="OK",
background_color=(0.3, 0.7, 0.3, 1)
)
layout.add_widget(prod_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)
btn_layout.add_widget(open_folder_btn)
btn_layout.add_widget(ok_btn)
layout.add_widget(btn_layout)
popup = Popup(
title="Select Video Generation Mode",
title=title,
content=layout,
size_hint=(0.9, 0.8),
size_hint=(0.9, 0.6),
auto_dismiss=False
)
def start_test_mode(instance):
def open_folder(instance):
folder_path = os.path.dirname(file_path)
os.system(f'xdg-open "{folder_path}"') # Linux
popup.dismiss()
self.generate_3d_video_test_mode()
def start_production_mode(instance):
def close_popup(instance):
popup.dismiss()
self.generate_3d_video_production_mode()
test_btn.bind(on_press=start_test_mode)
prod_btn.bind(on_press=start_production_mode)
cancel_btn.bind(on_press=lambda x: 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()

View File

@@ -0,0 +1,633 @@
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
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 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 Navigation Mode
advanced_layout = BoxLayout(orientation='vertical', spacing=5)
advanced_title = Label(
text="🧭 Navigation Animation",
font_size=16,
size_hint_y=None,
height=30,
color=(0.2, 0.6, 0.9, 1)
)
advanced_desc = Label(
text="• Satellite terrain details\n• 3D camera following at 1000-2000m\n• Google Earth entry scene\n• Professional navigation view",
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 Navigation Animation",
background_color=(0.2, 0.6, 0.9, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(advanced_btn)
# Google Earth Flythrough Mode
google_earth_layout = BoxLayout(orientation='vertical', spacing=5)
google_earth_title = Label(
text="🌍 Google Earth Flythrough",
font_size=16,
size_hint_y=None,
height=30,
color=(0.1, 0.8, 0.1, 1)
)
google_earth_desc = Label(
text="• Realistic 3D terrain with mountains\n• Cinematic camera following at 1000-2000m\n• Google Earth-style flythrough\n• Professional geographic animation",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
google_earth_desc.text_size = (None, None)
google_earth_layout.add_widget(google_earth_title)
google_earth_layout.add_widget(google_earth_desc)
layout.add_widget(google_earth_layout)
# Google Earth button
google_earth_btn = Button(
text="Generate Google Earth Flythrough",
background_color=(0.1, 0.8, 0.1, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(google_earth_btn)
# Blender Cinema Mode
blender_layout = BoxLayout(orientation='vertical', spacing=5)
blender_title = Label(
text="<22> 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_google_earth(instance):
popup.dismiss()
self.generate_google_earth_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)
google_earth_btn.bind(on_press=start_google_earth)
blender_btn.bind(on_press=start_blender_animation)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
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 generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation with terrain"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Google Earth flythrough...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Google Earth Flythrough",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_google_earth_animation():
try:
# Update status
def update_status(progress_val, status_text):
def _update(dt):
progress.value = progress_val
label.text = status_text
Clock.schedule_once(_update, 0)
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_path):
update_status(0, "Error: No GPS data found")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(10, "Checking dependencies...")
# Check dependencies first
generator = NavigationAnimationGenerator(project_folder)
generator.check_dependencies()
update_status(20, "Loading GPS data...")
df = generator.load_gps_data(positions_path)
update_status(30, "Generating navigation flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_navigation_flythrough_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
# Progress callback for the generator
def generator_progress(progress, message):
update_status(30 + (progress * 0.5), message) # Map 0-100% to 30-80%
update_status(80, "Creating navigation flythrough...")
success = generator.generate_3d_animation(
positions_path,
output_video_path,
style='advanced',
progress_callback=generator_progress
)
if success:
update_status(100, "Navigation flythrough complete!")
output_path = output_video_path
else:
raise Exception("Failed to generate navigation flythrough")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Google Earth Flythrough Complete!",
f"Your cinematic flythrough has been created:\n{output_path}",
output_path
)
Clock.schedule_once(show_success, 1)
except Exception as e:
error_message = str(e)
def show_error(dt):
popup.dismiss()
self.show_error_popup("Google Earth Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
def show_success_popup(self, title, message, file_path=None):
"""Show success popup with option to open file location"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
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}")

View File

@@ -0,0 +1,419 @@
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
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_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"""
# 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 generate Blender animation")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Blender Cinema Animation Complete!",
f"Your cinema-quality 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("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):
"""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()

View File

@@ -722,7 +722,7 @@
height: 202
size_hint_x: 1
# 3D Video Animation frame
# Progressive 3D Animation frame
BoxLayout:
orientation: "horizontal"
size_hint_y: None
@@ -737,7 +737,7 @@
size: self.size
Label:
text: "Generate 3D video animation\nsimilar to Relive style"
text: "Generate progressive 3D animation\nBuilds trip point by point"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
@@ -746,15 +746,84 @@
text_size: self.size
Button:
text: "Generate\n3D Video"
text: "Generate\n3D Trip"
size_hint_x: 0.3
font_size: 14
background_color: 0.8, 0.2, 0.4, 1
background_color: 0.2, 0.8, 0.4, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_3d_video()
on_press: root.generate_progressive_3d_animation()
# Google Earth Animation frame
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: 60
padding: [10, 10, 10, 10]
spacing: 10
canvas.before:
Color:
rgba: 0.15, 0.15, 0.18, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: "Generate Google Earth flythrough\nCinematic aerial view"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "Generate\nFlythrough"
size_hint_x: 0.3
font_size: 14
background_color: 0.1, 0.8, 0.1, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_google_earth_animation()
# Blender Animation frame
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: 60
padding: [10, 10, 10, 10]
spacing: 10
canvas.before:
Color:
rgba: 0.15, 0.15, 0.18, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: "Generate cinema-quality animation\nProfessional 3D rendering"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "Generate\nCinema"
size_hint_x: 0.3
font_size: 14
background_color: 0.9, 0.6, 0.2, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_blender_animation()
Widget: